2021-03-26 04:17:02 +04:30
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
2021-02-21 04:27:41 +03:30
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
2023-05-18 13:17:41 +02:00
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
2021-02-21 04:27:41 +03:30
|
|
|
|
2024-03-02 15:23:12 +01:00
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
2021-09-09 17:51:22 +04:30
|
|
|
import Button from '@mui/material/Button';
|
|
|
|
|
import DialogTitle from '@mui/material/DialogTitle';
|
|
|
|
|
import DialogContent from '@mui/material/DialogContent';
|
|
|
|
|
import DialogActions from '@mui/material/DialogActions';
|
|
|
|
|
import Dialog from '@mui/material/Dialog';
|
|
|
|
|
import FormGroup from '@mui/material/FormGroup';
|
2023-03-23 13:59:32 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2024-03-02 15:23:12 +01:00
|
|
|
import { Link } from 'react-router-dom';
|
2024-04-14 15:50:12 +02:00
|
|
|
import Stack from '@mui/material/Stack';
|
2024-10-06 01:27:51 +02:00
|
|
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
2025-08-15 22:02:58 +02:00
|
|
|
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
2025-10-04 00:14:11 +02:00
|
|
|
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts';
|
2025-08-16 02:48:46 +02:00
|
|
|
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
|
2025-08-15 22:02:58 +02:00
|
|
|
import { Categories } from '@/features/category/services/Categories.ts';
|
2025-08-16 02:48:46 +02:00
|
|
|
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
|
|
|
|
import { makeToast } from '@/base/utils/Toast.ts';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
2025-08-15 22:02:58 +02:00
|
|
|
import { updateMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
2024-07-08 18:02:50 +02:00
|
|
|
import {
|
|
|
|
|
GetCategoriesBaseQuery,
|
|
|
|
|
GetCategoriesBaseQueryVariables,
|
|
|
|
|
GetMangaCategoriesQuery,
|
|
|
|
|
GetMangaCategoriesQueryVariables,
|
|
|
|
|
} from '@/lib/graphql/generated/graphql.ts';
|
2024-07-01 21:40:27 +02:00
|
|
|
import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts';
|
2024-07-08 18:02:50 +02:00
|
|
|
import { GET_MANGA_CATEGORIES } from '@/lib/graphql/queries/MangaQuery.ts';
|
2025-08-16 02:48:46 +02:00
|
|
|
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
2024-12-20 17:24:40 +01:00
|
|
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
2021-02-21 04:27:41 +03:30
|
|
|
|
2023-12-25 21:37:45 +01:00
|
|
|
type BaseProps = {
|
2023-02-06 10:06:33 +01:00
|
|
|
open: boolean;
|
2024-03-02 15:26:15 +01:00
|
|
|
onClose: (didUpdateCategories: boolean, addToCategories?: number[], removeFromCategories?: number[]) => void;
|
2023-12-25 21:37:45 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SingleMangaModeProps = {
|
2023-02-06 10:06:33 +01:00
|
|
|
mangaId: number;
|
2023-12-27 18:08:16 +01:00
|
|
|
addToLibrary?: boolean;
|
2023-12-25 21:37:45 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MultiMangaModeProps = {
|
|
|
|
|
mangaIds: number[];
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-07 15:33:08 +02:00
|
|
|
export type CategorySelectProps =
|
2023-12-25 21:37:45 +01:00
|
|
|
| (BaseProps & SingleMangaModeProps & PropertiesNever<MultiMangaModeProps>)
|
|
|
|
|
| (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps);
|
|
|
|
|
|
|
|
|
|
const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => {
|
2024-07-08 18:02:50 +02:00
|
|
|
const { data: mangaResult } = requestManager.useGetManga<GetMangaCategoriesQuery, GetMangaCategoriesQueryVariables>(
|
|
|
|
|
GET_MANGA_CATEGORIES,
|
|
|
|
|
mangaId ?? -1,
|
|
|
|
|
{ skip: mangaId === undefined },
|
|
|
|
|
);
|
2023-12-25 21:37:45 +01:00
|
|
|
|
|
|
|
|
return useMemo(() => {
|
|
|
|
|
if (mangaId === undefined || !mangaResult) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 15:23:12 +01:00
|
|
|
return Categories.getIds(mangaResult.manga.categories.nodes);
|
2023-12-25 21:37:45 +01:00
|
|
|
}, [mangaResult?.manga.categories.nodes, mangaId]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCategoryCheckedState = (
|
|
|
|
|
categoryId: number,
|
|
|
|
|
categoriesToAdd: number[],
|
|
|
|
|
categoriesToRemove: number[],
|
|
|
|
|
isSingleSelectionMode: boolean,
|
|
|
|
|
): boolean | undefined => {
|
|
|
|
|
if (categoriesToAdd.includes(categoryId)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isSingleSelectionMode) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (categoriesToRemove.includes(categoryId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-02-21 04:27:41 +03:30
|
|
|
|
2023-12-25 21:37:45 +01:00
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-07 15:33:08 +02:00
|
|
|
export function CategorySelect(props: CategorySelectProps) {
|
2023-03-23 13:59:32 +01:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
2023-12-27 18:08:16 +01:00
|
|
|
const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props;
|
2023-12-25 21:37:45 +01:00
|
|
|
|
|
|
|
|
const isSingleSelectionMode = mangaId !== undefined;
|
|
|
|
|
const mangaIds = passedMangaIds ?? [mangaId];
|
2021-02-21 04:27:41 +03:30
|
|
|
|
2024-03-02 15:23:12 +01:00
|
|
|
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
|
|
|
|
|
|
2023-12-27 17:20:28 +01:00
|
|
|
const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
|
2024-07-08 18:02:50 +02:00
|
|
|
|
2024-07-01 21:40:27 +02:00
|
|
|
const { data } = requestManager.useGetCategories<GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables>(
|
|
|
|
|
GET_CATEGORIES_BASE,
|
|
|
|
|
);
|
2023-09-01 20:39:30 +02:00
|
|
|
const categoriesData = data?.categories.nodes;
|
2022-11-02 21:48:22 +01:00
|
|
|
|
2024-03-02 15:23:12 +01:00
|
|
|
const allCategories = useMemo(() => Categories.getUserCreated(categoriesData ?? []), [categoriesData]);
|
2021-02-21 04:27:41 +03:30
|
|
|
|
2023-12-27 18:08:16 +01:00
|
|
|
const defaultCategoryIds = useMemo(
|
2024-03-02 15:23:12 +01:00
|
|
|
() => (addToLibrary ? Categories.getIds(Categories.getDefaults(allCategories)) : []),
|
2023-12-27 18:08:16 +01:00
|
|
|
[allCategories],
|
|
|
|
|
);
|
|
|
|
|
|
2023-12-25 21:37:45 +01:00
|
|
|
const { handleSelection, setSelectionForKey, getSelectionForKey } = useSelectableCollection<
|
|
|
|
|
number,
|
|
|
|
|
'categoriesToAdd' | 'categoriesToRemove'
|
|
|
|
|
>(allCategories.length, {
|
|
|
|
|
currentKey: 'categoriesToAdd',
|
|
|
|
|
initialState: {
|
2023-12-27 18:08:16 +01:00
|
|
|
categoriesToAdd: [...mangaCategoryIds, ...defaultCategoryIds],
|
2023-12-25 21:37:45 +01:00
|
|
|
categoriesToRemove: [],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2023-12-27 17:20:28 +01:00
|
|
|
useEffect(() => {
|
2023-12-27 18:08:16 +01:00
|
|
|
setSelectionForKey('categoriesToAdd', [...mangaCategoryIds, ...defaultCategoryIds]);
|
2023-12-27 17:20:28 +01:00
|
|
|
setSelectionForKey('categoriesToRemove', []);
|
|
|
|
|
}, [mangaCategoryIds]);
|
|
|
|
|
|
2023-12-25 21:37:45 +01:00
|
|
|
const categoriesToAdd = getSelectionForKey('categoriesToAdd');
|
|
|
|
|
const categoriesToRemove = getSelectionForKey('categoriesToRemove');
|
2021-02-21 04:27:41 +03:30
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
2023-12-27 17:20:28 +01:00
|
|
|
setSelectionForKey('categoriesToAdd', mangaCategoryIds);
|
2023-12-25 21:37:45 +01:00
|
|
|
setSelectionForKey('categoriesToRemove', []);
|
2023-12-27 18:08:16 +01:00
|
|
|
onClose(false);
|
2021-02-21 04:27:41 +03:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOk = () => {
|
2023-12-25 21:37:45 +01:00
|
|
|
const addToCategories = isSingleSelectionMode
|
2023-12-27 17:20:28 +01:00
|
|
|
? categoriesToAdd.filter((categoryId) => !mangaCategoryIds.includes(categoryId))
|
2023-12-25 21:37:45 +01:00
|
|
|
: categoriesToAdd;
|
|
|
|
|
const removeFromCategories = isSingleSelectionMode
|
2023-12-27 17:20:28 +01:00
|
|
|
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
|
2023-12-25 21:37:45 +01:00
|
|
|
: categoriesToRemove;
|
|
|
|
|
|
2024-03-02 15:26:15 +01:00
|
|
|
onClose(true, addToCategories, removeFromCategories);
|
|
|
|
|
|
2024-03-02 15:23:12 +01:00
|
|
|
if (doNotShowAddToLibraryDialogAgain) {
|
2024-12-20 17:24:40 +01:00
|
|
|
updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch((e) =>
|
|
|
|
|
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
|
2024-03-16 16:21:15 +01:00
|
|
|
);
|
2024-03-02 15:23:12 +01:00
|
|
|
}
|
|
|
|
|
|
2023-12-25 21:37:45 +01:00
|
|
|
const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length;
|
|
|
|
|
if (!isUpdateRequired) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 15:26:15 +01:00
|
|
|
if (addToLibrary) {
|
|
|
|
|
// categories get updated in MangaDetails
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-25 21:37:45 +01:00
|
|
|
Mangas.performAction('change_categories', mangaIds, {
|
|
|
|
|
changeCategoriesPatch: {
|
|
|
|
|
addToCategories,
|
|
|
|
|
removeFromCategories,
|
2023-09-01 20:33:35 +02:00
|
|
|
},
|
2024-03-02 15:28:58 +01:00
|
|
|
}).catch(defaultPromiseErrorHandler('CategorySelect::handleOk'));
|
2021-02-21 04:27:41 +03:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog
|
2021-11-15 19:32:25 +03:30
|
|
|
sx={{
|
|
|
|
|
'.MuiDialog-paper': {
|
|
|
|
|
maxHeight: 435,
|
|
|
|
|
width: '80%',
|
|
|
|
|
},
|
|
|
|
|
}}
|
2021-02-21 04:27:41 +03:30
|
|
|
maxWidth="xs"
|
|
|
|
|
open={open}
|
2024-11-28 12:36:20 +01:00
|
|
|
onClose={handleCancel}
|
2021-02-21 04:27:41 +03:30
|
|
|
>
|
2023-03-23 13:59:32 +01:00
|
|
|
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
2021-02-21 04:27:41 +03:30
|
|
|
<DialogContent dividers>
|
|
|
|
|
<FormGroup>
|
2024-03-02 15:23:12 +01:00
|
|
|
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
|
2022-11-02 21:48:22 +01:00
|
|
|
{allCategories.map((category) => (
|
2023-12-25 21:37:45 +01:00
|
|
|
<ThreeStateCheckboxInput
|
|
|
|
|
checked={getCategoryCheckedState(
|
|
|
|
|
category.id,
|
|
|
|
|
categoriesToAdd,
|
|
|
|
|
categoriesToRemove,
|
|
|
|
|
isSingleSelectionMode,
|
|
|
|
|
)}
|
|
|
|
|
onChange={(checked) => {
|
2024-04-05 02:03:00 +02:00
|
|
|
handleSelection(category.id, false, { key: 'categoriesToAdd' });
|
|
|
|
|
handleSelection(category.id, false, { key: 'categoriesToRemove' });
|
2023-12-25 21:37:45 +01:00
|
|
|
|
|
|
|
|
if (checked) {
|
2024-04-05 02:03:00 +02:00
|
|
|
handleSelection(category.id, true, { key: 'categoriesToAdd' });
|
2023-12-25 21:37:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (checked === false) {
|
2024-04-05 02:03:00 +02:00
|
|
|
handleSelection(category.id, true, { key: 'categoriesToRemove' });
|
2023-12-25 21:37:45 +01:00
|
|
|
}
|
|
|
|
|
}}
|
2022-11-02 21:48:22 +01:00
|
|
|
label={category.name}
|
|
|
|
|
key={category.id}
|
2021-02-21 04:27:41 +03:30
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</FormGroup>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
2024-03-02 15:23:12 +01:00
|
|
|
<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)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2024-09-03 17:15:47 +02:00
|
|
|
<Stack
|
|
|
|
|
direction="row"
|
|
|
|
|
sx={{
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'end',
|
|
|
|
|
width: '100%',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2024-12-11 20:10:59 +01:00
|
|
|
<Button component={Link} to={AppRoutes.settings.childRoutes.categories.path}>
|
2024-03-02 15:23:12 +01:00
|
|
|
{t(allCategories.length ? 'global.button.edit' : 'global.button.create')}
|
|
|
|
|
</Button>
|
|
|
|
|
<Stack direction="row">
|
|
|
|
|
<Button autoFocus onClick={handleCancel} color="primary">
|
|
|
|
|
{t('global.button.cancel')}
|
|
|
|
|
</Button>
|
|
|
|
|
{!!allCategories.length && (
|
|
|
|
|
<Button onClick={handleOk} color="primary">
|
|
|
|
|
{t('global.button.ok')}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Stack>
|
|
|
|
|
</Stack>
|
|
|
|
|
</Stack>
|
2021-02-21 04:27:41 +03:30
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|