Replace "useCategorySelect" with "GlobalDialogManager"

This commit is contained in:
schroda
2025-10-04 14:47:38 +02:00
parent d33f708104
commit b795f98766
8 changed files with 101 additions and 162 deletions

View File

@@ -37,8 +37,8 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
type BaseProps = { type BaseProps = {
open: boolean; onCancel: () => void;
onClose: (didUpdateCategories: boolean, addToCategories?: number[], removeFromCategories?: number[]) => void; onConfirm: (selectedCategories: { addToCategories?: number[]; removeFromCategories?: number[] }) => void;
}; };
type SingleMangaModeProps = { type SingleMangaModeProps = {
@@ -94,7 +94,7 @@ const getCategoryCheckedState = (
export function CategorySelect(props: CategorySelectProps) { export function CategorySelect(props: CategorySelectProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props; const { onCancel, onConfirm, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props;
const isSingleSelectionMode = mangaId !== undefined; const isSingleSelectionMode = mangaId !== undefined;
const mangaIds = passedMangaIds ?? [mangaId]; const mangaIds = passedMangaIds ?? [mangaId];
@@ -137,7 +137,7 @@ export function CategorySelect(props: CategorySelectProps) {
const handleCancel = () => { const handleCancel = () => {
setSelectionForKey('categoriesToAdd', mangaCategoryIds); setSelectionForKey('categoriesToAdd', mangaCategoryIds);
setSelectionForKey('categoriesToRemove', []); setSelectionForKey('categoriesToRemove', []);
onClose(false); onCancel();
}; };
const handleOk = () => { const handleOk = () => {
@@ -148,7 +148,10 @@ export function CategorySelect(props: CategorySelectProps) {
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId)) ? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
: categoriesToRemove; : categoriesToRemove;
onClose(true, addToCategories, removeFromCategories); onConfirm({
addToCategories,
removeFromCategories,
});
if (doNotShowAddToLibraryDialogAgain) { if (doNotShowAddToLibraryDialogAgain) {
updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch((e) => updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch((e) =>
@@ -183,7 +186,7 @@ export function CategorySelect(props: CategorySelectProps) {
}, },
}} }}
maxWidth="xs" maxWidth="xs"
open={open} open
onClose={handleCancel} onClose={handleCancel}
> >
<DialogTitle>{t('category.title.set_categories')}</DialogTitle> <DialogTitle>{t('category.title.set_categories')}</DialogTitle>

View File

@@ -1,43 +0,0 @@
/*
* 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 { useMemo, useState } from 'react';
import { CategorySelect, CategorySelectProps } from '@/features/category/components/CategorySelect.tsx';
export const useCategorySelect = ({
mangaId,
mangaIds,
onClose,
addToLibrary,
}: Omit<CategorySelectProps, 'open' | 'onClose'> & Pick<Partial<CategorySelectProps>, 'onClose'>) => {
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
const CategorySelectComponent = useMemo(() => {
if (!isCategorySelectOpen) {
return null;
}
return (
<CategorySelect
open={isCategorySelectOpen}
onClose={(...args) => {
setIsCategorySelectOpen(false);
onClose?.(...args);
}}
mangaId={mangaId!} // either mangaId or mangaIds is undefined, however, ts is not able to infer it correctly and raises an error
mangaIds={mangaIds as undefined}
addToLibrary={addToLibrary}
/>
);
}, [mangaId, mangaIds, addToLibrary, onClose, isCategorySelectOpen]);
return {
openCategorySelect: setIsCategorySelectOpen,
CategorySelectComponent,
};
};

View File

@@ -29,17 +29,16 @@ import {
} from '@/base/components/menu/Menu.utils.ts'; } from '@/base/components/menu/Menu.utils.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { TrackManga } from '@/features/tracker/components/TrackManga.tsx'; import { TrackManga } from '@/features/tracker/components/TrackManga.tsx';
import { useCategorySelect } from '@/features/category/hooks/useCategorySelect.tsx';
import { ChaptersDownloadActionMenuItems } from '@/features/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx'; import { ChaptersDownloadActionMenuItems } from '@/features/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx';
import { NestedMenuItem } from '@/base/components/menu/NestedMenuItem.tsx'; import { NestedMenuItem } from '@/base/components/menu/NestedMenuItem.tsx';
import { MangaChapterStatFieldsFragment, MangaType } from '@/lib/graphql/generated/graphql.ts'; import { MangaChapterStatFieldsFragment, MangaType } from '@/lib/graphql/generated/graphql.ts';
import { MangaAction, MangaDownloadInfo, MangaIdInfo, MangaUnreadInfo } from '@/features/manga/Manga.types.ts'; import { MangaAction, MangaDownloadInfo, MangaIdInfo, MangaUnreadInfo } from '@/features/manga/Manga.types.ts';
import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts'; import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { GlobalDialogManager } from '@/base/global-dialog/GlobalDialogManager.tsx';
import { CategorySelect } from '@/features/category/components/CategorySelect.tsx';
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const; type BaseProps = { onClose: () => void; setHideMenu: (hide: boolean) => void };
type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void };
export type SingleModeProps = { export type SingleModeProps = {
manga: Pick<MangaType, 'id' | 'title' | 'sourceId'> & MangaDownloadInfo & MangaUnreadInfo; manga: Pick<MangaType, 'id' | 'title' | 'sourceId'> & MangaDownloadInfo & MangaUnreadInfo;
@@ -77,16 +76,9 @@ export const MangaActionMenuItems = ({
const hasUnreadChapters = !!manga?.unreadCount; const hasUnreadChapters = !!manga?.unreadCount;
const hasReadChapters = !!manga && manga.unreadCount !== manga.chapters.totalCount; const hasReadChapters = !!manga && manga.unreadCount !== manga.chapters.totalCount;
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
mangaId: manga?.id,
mangaIds: passedSelectedMangas ? Mangas.getIds(selectedMangas) : undefined,
onClose: () => onClose(true),
addToLibrary: false,
});
const handleSelect = () => { const handleSelect = () => {
handleSelection?.(manga.id, true); handleSelection?.(manga.id, true);
onClose(true); onClose();
}; };
const performAction = (action: MangaAction, mangas: MangaIdInfo[]) => { const performAction = (action: MangaAction, mangas: MangaIdInfo[]) => {
@@ -94,7 +86,7 @@ export const MangaActionMenuItems = ({
wasManuallyMarkedAsRead: true, wasManuallyMarkedAsRead: true,
}).catch(defaultPromiseErrorHandler(`MangaActionMenuItems:performAction(${action})`)); }).catch(defaultPromiseErrorHandler(`MangaActionMenuItems:performAction(${action})`));
onClose(!ACTION_DISABLES_SELECTION_MODE.includes(action)); onClose();
}; };
const { downloadableMangas, downloadedMangas, unreadMangas, readMangas } = useMemo( const { downloadableMangas, downloadedMangas, unreadMangas, readMangas } = useMemo(
@@ -127,7 +119,7 @@ export const MangaActionMenuItems = ({
> >
<ChaptersDownloadActionMenuItems <ChaptersDownloadActionMenuItems
mangaIds={isSingleMode ? [manga.id] : Mangas.getIds(selectedMangas)} mangaIds={isSingleMode ? [manga.id] : Mangas.getIds(selectedMangas)}
closeMenu={() => onClose(true)} closeMenu={onClose}
/> />
</NestedMenuItem> </NestedMenuItem>
)} )}
@@ -176,8 +168,13 @@ export const MangaActionMenuItems = ({
)} )}
<MenuItem <MenuItem
onClick={() => { onClick={() => {
openCategorySelect(true); GlobalDialogManager.show(CategorySelect, {
mangaId: manga?.id,
mangaIds: passedSelectedMangas ? Mangas.getIds(selectedMangas) : undefined,
addToLibrary: false,
});
setHideMenu(true); setHideMenu(true);
onClose();
}} }}
Icon={Label} Icon={Label}
title={getMenuItemTitle('change_categories', selectedMangas.length)} title={getMenuItemTitle('change_categories', selectedMangas.length)}
@@ -187,7 +184,6 @@ export const MangaActionMenuItems = ({
Icon={FavoriteBorderIcon} Icon={FavoriteBorderIcon}
title={getMenuItemTitle('remove_from_library', selectedMangas.length)} title={getMenuItemTitle('remove_from_library', selectedMangas.length)}
/> />
{CategorySelectComponent}
{isTrackDialogOpen && ( {isTrackDialogOpen && (
<Dialog <Dialog
open open
@@ -196,7 +192,7 @@ export const MangaActionMenuItems = ({
scroll="paper" scroll="paper"
onClose={() => { onClose={() => {
setIsTrackDialogOpen(false); setIsTrackDialogOpen(false);
onClose(true); onClose();
}} }}
> >
<TrackManga manga={manga!} /> <TrackManga manga={manga!} />

View File

@@ -21,9 +21,10 @@ import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { useCategorySelect } from '@/features/category/hooks/useCategorySelect.tsx';
import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { GlobalDialogManager } from '@/base/global-dialog/GlobalDialogManager.tsx';
import { CategorySelect } from '@/features/category/components/CategorySelect.tsx';
interface IProps { interface IProps {
manga: Pick<MangaType, 'id' | 'inLibrary' | 'sourceId' | 'title'>; manga: Pick<MangaType, 'id' | 'inLibrary' | 'sourceId' | 'title'>;
@@ -43,9 +44,9 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
setAnchorEl(null); setAnchorEl(null);
}; };
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({ const openCategorySelection = () => {
mangaId: manga.id, GlobalDialogManager.show(CategorySelect, { mangaId: manga.id });
}); };
return ( return (
<> <>
@@ -82,7 +83,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<CustomTooltip title={t('manga.label.edit_categories')}> <CustomTooltip title={t('manga.label.edit_categories')}>
<IconButton <IconButton
onClick={() => { onClick={() => {
openCategorySelect(true); openCategorySelection();
}} }}
color="inherit" color="inherit"
> >
@@ -142,7 +143,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<MenuItem <MenuItem
key="categories" key="categories"
onClick={() => { onClick={() => {
openCategorySelect(true); openCategorySelection();
handleClose(); handleClose();
}} }}
> >
@@ -155,8 +156,6 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
</Menu> </Menu>
</> </>
)} )}
{CategorySelectComponent}
</> </>
); );
}; };

View File

@@ -49,10 +49,7 @@ export const MangaCard = memo((props: MangaCardProps) => {
settings: { showContinueReadingButton }, settings: { showContinueReadingButton },
} = useMetadataServerSettings(); } = useMetadataServerSettings();
const { CategorySelectComponent, updateLibraryState, isInLibrary } = useManageMangaLibraryState( const { updateLibraryState, isInLibrary } = useManageMangaLibraryState(manga, mode === 'source');
manga,
mode === 'source',
);
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title); const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title);
@@ -158,7 +155,6 @@ export const MangaCard = memo((props: MangaCardProps) => {
)} )}
</Menu> </Menu>
)} )}
{CategorySelectComponent}
</> </>
)} )}
</PopupState> </PopupState>

View File

@@ -227,7 +227,7 @@ export const MangaDetails = ({
} }
}, [manga.source]); }, [manga.source]);
const { CategorySelectComponent, updateLibraryState } = useManageMangaLibraryState(manga); const { updateLibraryState } = useManageMangaLibraryState(manga);
const copyTitle = async () => { const copyTitle = async () => {
try { try {
@@ -239,59 +239,56 @@ export const MangaDetails = ({
}; };
return ( return (
<> <DetailsWrapper>
<DetailsWrapper> <TopContentWrapper url={Mangas.getThumbnailUrl(manga)} mangaThumbnailBackdrop={mangaThumbnailBackdrop}>
<TopContentWrapper url={Mangas.getThumbnailUrl(manga)} mangaThumbnailBackdrop={mangaThumbnailBackdrop}> <ThumbnailMetadataWrapper>
<ThumbnailMetadataWrapper> <Thumbnail manga={manga} mangaDynamicColorSchemes={mangaDynamicColorSchemes} />
<Thumbnail manga={manga} mangaDynamicColorSchemes={mangaDynamicColorSchemes} /> <MetadataContainer>
<MetadataContainer> <Stack sx={{ flexDirection: 'row', gap: 1, alignItems: 'flex-start', mb: 1 }}>
<Stack sx={{ flexDirection: 'row', gap: 1, alignItems: 'flex-start', mb: 1 }}> <SearchLink query={manga.title} sourceId={manga.sourceId} mode="source.global-search">
<SearchLink query={manga.title} sourceId={manga.sourceId} mode="source.global-search"> <Typography variant="h5" component="h2" sx={{ wordBreak: 'break-word' }}>
<Typography variant="h5" component="h2" sx={{ wordBreak: 'break-word' }}> {manga.title}
{manga.title} </Typography>
</Typography> </SearchLink>
</SearchLink> <CustomTooltip title={t('global.button.copy')}>
<CustomTooltip title={t('global.button.copy')}> <IconButton onClick={copyTitle} color="inherit">
<IconButton onClick={copyTitle} color="inherit"> <ContentCopyIcon fontSize="small" />
<ContentCopyIcon fontSize="small" /> </IconButton>
</IconButton> </CustomTooltip>
</CustomTooltip> </Stack>
</Stack> {manga.author && (
{manga.author && (
<Metadata
title={t('manga.label.author')}
value={valuesToJoinedSearchLinks(Mangas.getAuthors(manga), manga.source?.id, mode)}
/>
)}
{manga.artist && (
<Metadata
title={t('manga.label.artist')}
value={valuesToJoinedSearchLinks(Mangas.getArtists(manga), manga.source?.id, mode)}
/>
)}
<Metadata <Metadata
title={t('manga.label.status')} title={t('manga.label.author')}
value={t(MANGA_STATUS_TO_TRANSLATION[manga.status])} value={valuesToJoinedSearchLinks(Mangas.getAuthors(manga), manga.source?.id, mode)}
/> />
<Metadata title={t('source.title_one')} value={getSourceName(manga.source)} /> )}
</MetadataContainer> {manga.artist && (
</ThumbnailMetadataWrapper> <Metadata
<MangaButtonsContainer> title={t('manga.label.artist')}
<CustomButton value={valuesToJoinedSearchLinks(Mangas.getArtists(manga), manga.source?.id, mode)}
size="medium" />
onClick={updateLibraryState} )}
variant={manga.inLibrary ? 'contained' : 'outlined'} <Metadata
> title={t('manga.label.status')}
{manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />} value={t(MANGA_STATUS_TO_TRANSLATION[manga.status])}
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')} />
</CustomButton> <Metadata title={t('source.title_one')} value={getSourceName(manga.source)} />
<TrackMangaButton manga={manga} /> </MetadataContainer>
<OpenSourceButton url={manga.realUrl} /> </ThumbnailMetadataWrapper>
</MangaButtonsContainer> <MangaButtonsContainer>
</TopContentWrapper> <CustomButton
<DescriptionGenre manga={manga} mode={mode} /> size="medium"
</DetailsWrapper> onClick={updateLibraryState}
{CategorySelectComponent} variant={manga.inLibrary ? 'contained' : 'outlined'}
</> >
{manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
</CustomButton>
<TrackMangaButton manga={manga} />
<OpenSourceButton url={manga.realUrl} />
</MangaButtonsContainer>
</TopContentWrapper>
<DescriptionGenre manga={manga} mode={mode} />
</DetailsWrapper>
); );
}; };

View File

@@ -10,7 +10,6 @@ import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { useCategorySelect } from '@/features/category/hooks/useCategorySelect.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts'; import { makeToast } from '@/base/utils/Toast.ts';
import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
@@ -22,6 +21,7 @@ import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { GlobalDialogManager } from '@/base/global-dialog/GlobalDialogManager.tsx'; import { GlobalDialogManager } from '@/base/global-dialog/GlobalDialogManager.tsx';
import { CategorySelect } from '@/features/category/components/CategorySelect';
export const useManageMangaLibraryState = ( export const useManageMangaLibraryState = (
manga: Pick<MangaType, 'id' | 'title'> & Partial<Pick<MangaType, 'inLibrary'>>, manga: Pick<MangaType, 'id' | 'title'> & Partial<Pick<MangaType, 'inLibrary'>>,
@@ -33,11 +33,7 @@ export const useManageMangaLibraryState = (
const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary); const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary);
const addToLibrary = useCallback( const addToLibrary = useCallback(
(didSubmit: boolean, addToCategories: number[] = [], removeFromCategories: number[] = []) => { (addToCategories: number[] = [], removeFromCategories: number[] = []) => {
if (!didSubmit) {
return;
}
requestManager requestManager
.updateManga(manga.id, { .updateManga(manga.id, {
updateManga: { inLibrary: true }, updateManga: { inLibrary: true },
@@ -67,12 +63,6 @@ export const useManageMangaLibraryState = (
setIsInLibrary(false); setIsInLibrary(false);
}, [manga.id, confirmRemoval]); }, [manga.id, confirmRemoval]);
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
mangaId: manga.id,
addToLibrary: true,
onClose: addToLibrary,
});
const updateLibraryState = useCallback(() => { const updateLibraryState = useCallback(() => {
const update = async () => { const update = async () => {
if (isInLibrary) { if (isInLibrary) {
@@ -144,18 +134,26 @@ export const useManageMangaLibraryState = (
const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length; const showCategorySelectDialog = showAddToLibraryCategorySelectDialog && !!userCreatedCategories.length;
if (!showCategorySelectDialog) { if (!showCategorySelectDialog) {
addToLibrary(true, Categories.getIds(Categories.getDefaults(userCreatedCategories!))); addToLibrary(Categories.getIds(Categories.getDefaults(userCreatedCategories!)));
return; return;
} }
openCategorySelect(true); const { addToCategories, removeFromCategories } = await GlobalDialogManager.show(
`manga-library-state-add-categories-${manga.id}`,
CategorySelect,
{
mangaId: manga.id,
addToLibrary: true,
},
);
addToLibrary(addToCategories, removeFromCategories);
}; };
update().catch(defaultPromiseErrorHandler('useManageMangaLibraryState::updateLibraryState')); update().catch(defaultPromiseErrorHandler('useManageMangaLibraryState::updateLibraryState'));
}, [isInLibrary, removeFromLibrary, addToLibrary]); }, [isInLibrary, removeFromLibrary, addToLibrary]);
return { return {
CategorySelectComponent,
updateLibraryState, updateLibraryState,
/** /**
* In case of browsing the source, the data has to be fetched via a mutation. * In case of browsing the source, the data has to be fetched via a mutation.

View File

@@ -28,22 +28,15 @@ export const ReaderLibraryButton = memo(() => {
const { inLibrary } = manga ?? ACTION_FALLBACK_MANGA; const { inLibrary } = manga ?? ACTION_FALLBACK_MANGA;
const { t } = useTranslation(); const { t } = useTranslation();
const { CategorySelectComponent, updateLibraryState } = useManageMangaLibraryState( const { updateLibraryState } = useManageMangaLibraryState(manga ?? ACTION_FALLBACK_MANGA, true);
manga ?? ACTION_FALLBACK_MANGA,
true,
);
return ( return (
<> <CustomTooltip
<CustomTooltip title={inLibrary ? t('manga.action.library.remove.label.action') : t('manga.button.add_to_library')}
title={inLibrary ? t('manga.action.library.remove.label.action') : t('manga.button.add_to_library')} >
> <IconButton onClick={updateLibraryState} color="inherit">
<IconButton onClick={updateLibraryState} color="inherit"> {inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
{inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />} </IconButton>
</IconButton> </CustomTooltip>
</CustomTooltip>
{CategorySelectComponent}
</>
); );
}); });