Feature/improve add to library category selection dialog (#631)
* Create "Categories" util class * Only show add to library category selection in case categories exist * Add edit categories button in select categories dialog * Add option to disable add to library dialog to the dialog
This commit is contained in:
@@ -21,6 +21,8 @@ import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
|||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
||||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||||
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
|
import { Categories } from '@/lib/data/Categories.ts';
|
||||||
|
|
||||||
const DetailsWrapper = styled('div')(({ theme }) => ({
|
const DetailsWrapper = styled('div')(({ theme }) => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -177,6 +179,12 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
loading: areSettingsLoading,
|
loading: areSettingsLoading,
|
||||||
} = useMetadataServerSettings();
|
} = useMetadataServerSettings();
|
||||||
|
|
||||||
|
const categories = requestManager.useGetCategories();
|
||||||
|
const userCreatedCategories = useMemo(
|
||||||
|
() => Categories.getUserCreated(categories.data?.categories.nodes ?? []),
|
||||||
|
[categories.data?.categories.nodes],
|
||||||
|
);
|
||||||
|
|
||||||
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -195,7 +203,21 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToLibraryClick = () => {
|
const handleAddToLibraryClick = () => {
|
||||||
if (!showAddToLibraryCategorySelectDialog) {
|
if (categories.loading) {
|
||||||
|
makeToast(t('global.label.load_in_progress'), 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.error) {
|
||||||
|
makeToast(t('category.error.label.request_failure'), 'error');
|
||||||
|
categories
|
||||||
|
.refetch()
|
||||||
|
.catch(defaultPromiseErrorHandler('MangaDetails::handleAddToLibraryClick: refetch categories'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length;
|
||||||
|
if (!showCategorySelectDialog) {
|
||||||
addToLibrary();
|
addToLibrary();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -236,7 +258,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
|||||||
<MangaButtonsContainer inLibrary={manga.inLibrary}>
|
<MangaButtonsContainer inLibrary={manga.inLibrary}>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
disabled={areSettingsLoading}
|
disabled={areSettingsLoading || categories.loading}
|
||||||
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
||||||
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
|
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
|
||||||
size="large"
|
size="large"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
@@ -14,11 +14,17 @@ import DialogActions from '@mui/material/DialogActions';
|
|||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||||
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
||||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
|
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
|
||||||
import { TCategory } from '@/typings.ts';
|
import { Categories } from '@/lib/data/Categories.ts';
|
||||||
|
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
|
||||||
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.ts';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
|
||||||
type BaseProps = {
|
type BaseProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -46,7 +52,7 @@ const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return mangaResult.manga.categories.nodes.map((category) => category.id);
|
return Categories.getIds(mangaResult.manga.categories.nodes);
|
||||||
}, [mangaResult?.manga.categories.nodes, mangaId]);
|
}, [mangaResult?.manga.categories.nodes, mangaId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,9 +77,6 @@ const getCategoryCheckedState = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultCategoryIds = (categories: TCategory[]) =>
|
|
||||||
categories.filter(({ default: isDefault }) => isDefault).map(({ id }) => id);
|
|
||||||
|
|
||||||
export function CategorySelect(props: Props) {
|
export function CategorySelect(props: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -82,20 +85,17 @@ export function CategorySelect(props: Props) {
|
|||||||
const isSingleSelectionMode = mangaId !== undefined;
|
const isSingleSelectionMode = mangaId !== undefined;
|
||||||
const mangaIds = passedMangaIds ?? [mangaId];
|
const mangaIds = passedMangaIds ?? [mangaId];
|
||||||
|
|
||||||
|
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
|
||||||
|
const { metadata: serverMetadata } = useMetadataServerSettings();
|
||||||
|
|
||||||
const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
|
const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
|
||||||
const { data } = requestManager.useGetCategories();
|
const { data } = requestManager.useGetCategories();
|
||||||
const categoriesData = data?.categories.nodes;
|
const categoriesData = data?.categories.nodes;
|
||||||
|
|
||||||
const allCategories = useMemo(() => {
|
const allCategories = useMemo(() => Categories.getUserCreated(categoriesData ?? []), [categoriesData]);
|
||||||
const cats = [...(categoriesData ?? [])]; // make copy
|
|
||||||
if (cats.length > 0 && cats[0].name === 'Default') {
|
|
||||||
cats.shift(); // remove first category if it is 'Default'
|
|
||||||
}
|
|
||||||
return cats;
|
|
||||||
}, [categoriesData]);
|
|
||||||
|
|
||||||
const defaultCategoryIds = useMemo(
|
const defaultCategoryIds = useMemo(
|
||||||
() => (addToLibrary ? getDefaultCategoryIds(allCategories) : []),
|
() => (addToLibrary ? Categories.getIds(Categories.getDefaults(allCategories)) : []),
|
||||||
[allCategories],
|
[allCategories],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,6 +134,12 @@ export function CategorySelect(props: Props) {
|
|||||||
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
|
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
|
||||||
: categoriesToRemove;
|
: categoriesToRemove;
|
||||||
|
|
||||||
|
if (doNotShowAddToLibraryDialogAgain) {
|
||||||
|
requestUpdateServerMetadata(convertToGqlMeta(serverMetadata)! ?? {}, [
|
||||||
|
['showAddToLibraryCategorySelectDialog', false],
|
||||||
|
]).catch(() => makeToast(t('search.error.label.failed_to_save_settings'), 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length;
|
const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length;
|
||||||
if (!isUpdateRequired) {
|
if (!isUpdateRequired) {
|
||||||
return;
|
return;
|
||||||
@@ -161,13 +167,7 @@ export function CategorySelect(props: Props) {
|
|||||||
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{allCategories.length === 0 && (
|
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
|
||||||
<span>
|
|
||||||
{t('category.error.no_categories_found.label.info')}
|
|
||||||
<br />
|
|
||||||
{t('category.error.no_categories_found.label.hint')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{allCategories.map((category) => (
|
{allCategories.map((category) => (
|
||||||
<ThreeStateCheckboxInput
|
<ThreeStateCheckboxInput
|
||||||
checked={getCategoryCheckedState(
|
checked={getCategoryCheckedState(
|
||||||
@@ -195,12 +195,31 @@ export function CategorySelect(props: Props) {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
<Stack sx={{ width: '100%' }}>
|
||||||
|
{addToLibrary && (
|
||||||
|
<CheckboxInput
|
||||||
|
sx={{ margin: 0 }}
|
||||||
|
size="small"
|
||||||
|
label={t('global.button.dont_show_dialog_again')}
|
||||||
|
onChange={(e) => setDoNotShowAddToLibraryDialogAgain(e.target.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between" alignItems="end">
|
||||||
|
<Button component={Link} to="/settings/categories">
|
||||||
|
{t(allCategories.length ? 'global.button.edit' : 'global.button.create')}
|
||||||
|
</Button>
|
||||||
|
<Stack direction="row">
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
{t('global.button.cancel')}
|
{t('global.button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{!!allCategories.length && (
|
||||||
<Button onClick={handleOk} color="primary">
|
<Button onClick={handleOk} color="primary">
|
||||||
{t('global.button.ok')}
|
{t('global.button.ok')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
},
|
},
|
||||||
"no_categories_found": {
|
"no_categories_found": {
|
||||||
"label": {
|
"label": {
|
||||||
"hint": "You should make some from the settings.",
|
"info": "You don't have any categories yet."
|
||||||
"info": "No categories found."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -296,7 +295,9 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"create": "Create",
|
||||||
"deselect": "Deselect",
|
"deselect": "Deselect",
|
||||||
|
"dont_show_dialog_again": "Don't show this dialog again",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"latest": "Latest",
|
"latest": "Latest",
|
||||||
@@ -370,6 +371,7 @@
|
|||||||
"general": "General",
|
"general": "General",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"links": "Links",
|
"links": "Links",
|
||||||
|
"load_in_progress": "Still loading required data…",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
|
|||||||
28
src/lib/data/Categories.ts
Normal file
28
src/lib/data/Categories.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TCategory } from '@/typings.ts';
|
||||||
|
|
||||||
|
export const DEFAULT_CATEGORY_ID = 0;
|
||||||
|
|
||||||
|
export type CategoryIdInfo = Pick<TCategory, 'id'>;
|
||||||
|
export type CategoryDefaultInfo = Pick<TCategory, 'default'>;
|
||||||
|
|
||||||
|
export class Categories {
|
||||||
|
static getIds(categories: CategoryIdInfo[]): number[] {
|
||||||
|
return categories.map((category) => category.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUserCreated<Category extends CategoryIdInfo>(categories: Category[]): Category[] {
|
||||||
|
return categories.filter((category) => category.id !== DEFAULT_CATEGORY_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDefaults<Category extends CategoryDefaultInfo>(categories: Category[]): Category[] {
|
||||||
|
return categories.filter((category) => category.default);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user