/* * 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 { useState, useEffect, useMemo } from 'react'; 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'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import Stack from '@mui/material/Stack'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { Mangas } from '@/modules/manga/services/Mangas.ts'; import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts'; import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx'; import { Categories } from '@/modules/category/services/Categories.ts'; import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { updateMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables, GetMangaCategoriesQuery, GetMangaCategoriesQueryVariables, } from '@/lib/graphql/generated/graphql.ts'; import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_MANGA_CATEGORIES } from '@/lib/graphql/queries/MangaQuery.ts'; type BaseProps = { open: boolean; onClose: (didUpdateCategories: boolean, addToCategories?: number[], removeFromCategories?: number[]) => void; }; type SingleMangaModeProps = { mangaId: number; addToLibrary?: boolean; }; type MultiMangaModeProps = { mangaIds: number[]; }; export type CategorySelectProps = | (BaseProps & SingleMangaModeProps & PropertiesNever) | (BaseProps & PropertiesNever & MultiMangaModeProps); const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => { const { data: mangaResult } = requestManager.useGetManga( GET_MANGA_CATEGORIES, mangaId ?? -1, { skip: mangaId === undefined }, ); return useMemo(() => { if (mangaId === undefined || !mangaResult) { return []; } return Categories.getIds(mangaResult.manga.categories.nodes); }, [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; } return undefined; }; export function CategorySelect(props: CategorySelectProps) { const { t } = useTranslation(); const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props; const isSingleSelectionMode = mangaId !== undefined; const mangaIds = passedMangaIds ?? [mangaId]; const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false); const mangaCategoryIds = useGetMangaCategoryIds(mangaId); const { data } = requestManager.useGetCategories( GET_CATEGORIES_BASE, ); const categoriesData = data?.categories.nodes; const allCategories = useMemo(() => Categories.getUserCreated(categoriesData ?? []), [categoriesData]); const defaultCategoryIds = useMemo( () => (addToLibrary ? Categories.getIds(Categories.getDefaults(allCategories)) : []), [allCategories], ); const { handleSelection, setSelectionForKey, getSelectionForKey } = useSelectableCollection< number, 'categoriesToAdd' | 'categoriesToRemove' >(allCategories.length, { currentKey: 'categoriesToAdd', initialState: { categoriesToAdd: [...mangaCategoryIds, ...defaultCategoryIds], categoriesToRemove: [], }, }); useEffect(() => { setSelectionForKey('categoriesToAdd', [...mangaCategoryIds, ...defaultCategoryIds]); setSelectionForKey('categoriesToRemove', []); }, [mangaCategoryIds]); const categoriesToAdd = getSelectionForKey('categoriesToAdd'); const categoriesToRemove = getSelectionForKey('categoriesToRemove'); const handleCancel = () => { setSelectionForKey('categoriesToAdd', mangaCategoryIds); setSelectionForKey('categoriesToRemove', []); onClose(false); }; const handleOk = () => { const addToCategories = isSingleSelectionMode ? categoriesToAdd.filter((categoryId) => !mangaCategoryIds.includes(categoryId)) : categoriesToAdd; const removeFromCategories = isSingleSelectionMode ? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId)) : categoriesToRemove; onClose(true, addToCategories, removeFromCategories); if (doNotShowAddToLibraryDialogAgain) { updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch(() => makeToast(t('search.error.label.failed_to_save_settings'), 'error'), ); } const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length; if (!isUpdateRequired) { return; } if (addToLibrary) { // categories get updated in MangaDetails return; } Mangas.performAction('change_categories', mangaIds, { changeCategoriesPatch: { addToCategories, removeFromCategories, }, }).catch(defaultPromiseErrorHandler('CategorySelect::handleOk')); }; return ( {t('category.title.set_categories')} {allCategories.length === 0 && {t('category.error.no_categories_found.label.info')}} {allCategories.map((category) => ( { handleSelection(category.id, false, { key: 'categoriesToAdd' }); handleSelection(category.id, false, { key: 'categoriesToRemove' }); if (checked) { handleSelection(category.id, true, { key: 'categoriesToAdd' }); } if (checked === false) { handleSelection(category.id, true, { key: 'categoriesToRemove' }); } }} label={category.name} key={category.id} /> ))} {addToLibrary && ( setDoNotShowAddToLibraryDialogAgain(e.target.checked)} /> )} {!!allCategories.length && ( )} ); }