/* * 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 { useCallback, useEffect, useState } from 'react'; import gql from 'graphql-tag'; import { AwaitableComponent } from 'awaitable-component'; import { useLingui } from '@lingui/react/macro'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { Categories } from '@/features/category/services/Categories.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { Mangas } from '@/features/manga/services/Mangas.ts'; import { GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables, MangaType } from '@/lib/graphql/generated/graphql.ts'; import { GET_CATEGORIES_BASE } from '@/lib/graphql/category/CategoryQuery.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { CategorySelect } from '@/features/category/components/CategorySelect'; import { Confirmation } from '@/base/AppAwaitableComponent.ts'; export const useManageMangaLibraryState = ( manga: Pick & Partial>, confirmRemoval: boolean = false, ) => { const { t } = useLingui(); const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary); useEffect(() => { setIsInLibrary(!!manga.inLibrary); }, [manga.id]); const addToLibrary = useCallback( (addToCategories: number[] = [], removeFromCategories: number[] = []) => { requestManager .updateManga(manga.id, { updateManga: { inLibrary: true }, updateMangaCategories: { addToCategories, removeFromCategories }, }) .response.then(() => makeToast(t`Added manga to library!`, 'success')) .then(() => setIsInLibrary(true)) .catch((e) => { makeToast(t`Could not add manga to library!`, 'error', getErrorMessage(e)); }); }, [manga.id], ); const removeFromLibrary = useCallback(async () => { if (confirmRemoval) { await Confirmation.show( { title: t`Are you sure?`, message: t`You are about to remove "${manga.title}" from your library`, actions: { confirm: { title: t`Remove`, }, }, }, { id: `manga-library-state-remove-${manga.id}` }, ); } await Mangas.removeFromLibrary([manga.id], true); setIsInLibrary(false); }, [manga.id, confirmRemoval]); const updateLibraryState = useCallback(() => { const update = async () => { if (isInLibrary) { removeFromLibrary().catch( defaultPromiseErrorHandler('useManageMangaLibraryState::updateLibraryState::removeFromLibrary'), ); return; } let showAddToLibraryCategorySelectDialog: boolean; try { showAddToLibraryCategorySelectDialog = (await getMetadataServerSettings()) .showAddToLibraryCategorySelectDialog; } catch (e) { makeToast(t`Unable to load data`, 'error', getErrorMessage(e)); return; } let categories: Awaited< ReturnType< typeof requestManager.getCategories >['response'] >; try { categories = await requestManager.getCategories< GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables >(GET_CATEGORIES_BASE).response; } catch (e) { makeToast(t`Could not load categories`, 'error', getErrorMessage(e)); return; } const userCreatedCategories = Categories.getUserCreated(categories.data.categories.nodes); let duplicatedLibraryMangas: | Awaited['response']> | undefined; try { duplicatedLibraryMangas = await Mangas.getDuplicateLibraryMangas(manga.title).response; } catch (e) { const errorMessage = getErrorMessage(e); await Confirmation.show( { title: t`Unable to load data`, message: t`Could not check for duplicated manga in your library.\n\nError: ${errorMessage}`, actions: { extra: { show: true, title: t`Retry`, contain: true, }, confirm: { title: t`Add`, }, }, onExtra: () => update().catch( defaultPromiseErrorHandler('useManageMangaLibraryState::update: retry duplicate check'), ), }, { id: `manga-library-state-add-${manga.id}` }, ); } const doDuplicatesExist = duplicatedLibraryMangas?.data.mangas.totalCount; if (doDuplicatesExist) { await Confirmation.show( { title: t`Are you sure?`, message: t`You have an entry in your library with the same name.`, actions: { extra: { show: true, title: t`Show entry`, contain: true, link: AppRoutes.manga.path(duplicatedLibraryMangas!.data.mangas.nodes[0].id), }, confirm: { title: t`Add`, }, }, onExtra: () => {}, }, { id: `manga-library-state-add-duplicated-${manga.id}` }, ); } const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length; if (!showCategorySelectDialog) { addToLibrary(Categories.getIds(Categories.getDefaults(userCreatedCategories!))); return; } const { addToCategories, removeFromCategories } = await AwaitableComponent.show( CategorySelect, { mangaId: manga.id, addToLibrary: true, }, { id: `manga-library-state-add-categories-${manga.id}` }, ); addToLibrary(addToCategories, removeFromCategories); }; update().catch(defaultPromiseErrorHandler('useManageMangaLibraryState::updateLibraryState')); }, [isInLibrary, removeFromLibrary, addToLibrary]); return { updateLibraryState, /** * In case of browsing the source, the data has to be fetched via a mutation. * Thus, the source browse data never has the updated in library state unless it has to rerender, which does not get * triggered by updating the manga in this hook. * * To work around this issue, the currently known in library state gets returned here */ isInLibrary: Mangas.getFromCache( manga.id, gql` fragment MangaInLibraryState on MangaType { inLibrary } `, 'MangaInLibraryState', )?.inLibrary ?? isInLibrary, }; };