Feature/browse source manga long press remove add manga from library (#710)

* Extract CategorySelect usage into hook

* Extract logic to change manga in library state into hook

* Change manga in library state on long press when browsing source
This commit is contained in:
schroda
2024-04-07 15:33:08 +02:00
committed by GitHub
parent aab3e25031
commit 29860482a6
13 changed files with 380 additions and 113 deletions

View File

@@ -22,11 +22,11 @@ import { Dialog } from '@mui/material';
import { TManga } from '@/typings.ts';
import { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
import { MenuItem } from '@/components/menu/MenuItem.tsx';
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { TrackManga } from '@/components/tracker/TrackManga.tsx';
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
@@ -54,7 +54,6 @@ export const MangaActionMenuItems = ({
}: Props) => {
const { t } = useTranslation();
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
const [isTrackDialogOpen, setIsTrackDialogOpen] = useState(false);
const isSingleMode = !!manga;
@@ -69,6 +68,13 @@ export const MangaActionMenuItems = ({
const hasUnreadChapters = !!manga?.unreadCount;
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 = () => {
handleSelection?.(manga.id, true);
onClose(true);
@@ -156,7 +162,7 @@ export const MangaActionMenuItems = ({
)}
<MenuItem
onClick={() => {
setIsCategorySelectOpen(true);
openCategorySelect(true);
setHideMenu(true);
}}
Icon={Label}
@@ -167,17 +173,7 @@ export const MangaActionMenuItems = ({
Icon={FavoriteBorderIcon}
title={getMenuItemTitle('remove_from_library', selectedMangas.length)}
/>
{isCategorySelectOpen && (
<CategorySelect
open={isCategorySelectOpen}
onClose={() => {
setIsCategorySelectOpen(false);
onClose(true);
}}
mangaId={manga?.id as undefined} // either mangaId or mangaIds is undefined, however, ts is not able to infer it correctly and raises an error
mangaIds={(passedSelectedMangas ? Mangas.getIds(selectedMangas) : undefined) as number[]}
/>
)}
{CategorySelectComponent}
{isTrackDialogOpen && (
<Dialog
open

View File

@@ -10,21 +10,17 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import PublicIcon from '@mui/icons-material/Public';
import { styled } from '@mui/material/styles';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import { Link } from '@mui/material';
import { ISource, TManga } from '@/typings';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/components/util/Toast';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
import { Mangas } from '@/lib/data/Mangas.ts';
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { Categories } from '@/lib/data/Categories.ts';
import { CustomIconButton } from '@/components/atoms/CustomIconButton';
import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx';
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
const DetailsWrapper = styled('div')(({ theme }) => ({
width: '100%',
@@ -169,63 +165,13 @@ function getValueOrUnknown(val?: string | null) {
export const MangaDetails: React.FC<IProps> = ({ manga }) => {
const { t } = useTranslation();
const {
settings: { showAddToLibraryCategorySelectDialog },
loading: areSettingsLoading,
} = useMetadataServerSettings();
const categories = requestManager.useGetCategories();
const userCreatedCategories = useMemo(
() => Categories.getUserCreated(categories.data?.categories.nodes ?? []),
[categories.data?.categories.nodes],
);
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
useEffect(() => {
if (!manga.source) {
makeToast(translate('source.error.label.source_not_found'), 'error');
}
}, [manga.source]);
const addToLibrary = (addToCategories: number[] = [], removeFromCategories: number[] = []) => {
requestManager
.updateManga(manga.id, {
updateManga: { inLibrary: true },
updateMangaCategories: { addToCategories, removeFromCategories },
})
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
.catch(() => {
makeToast(t('library.error.label.add_to_library'), 'error');
});
};
const handleAddToLibraryClick = () => {
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(Categories.getIds(Categories.getDefaults(userCreatedCategories!)));
return;
}
setIsCategorySelectOpen(true);
};
const removeFromLibrary = () => {
Mangas.removeFromLibrary([manga.id]).catch(defaultPromiseErrorHandler('MangaDetails::removeFromLibrary'));
};
const { CategorySelectComponent, updateLibraryState } = useManageMangaLibraryState(manga);
return (
<>
@@ -251,8 +197,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
</ThumbnailMetadataWrapper>
<MangaButtonsContainer>
<CustomIconButton
disabled={areSettingsLoading || categories.loading}
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
onClick={updateLibraryState}
size="large"
sx={{ color: manga.inLibrary ? '#2196f3' : 'inherit' }}
>
@@ -275,20 +220,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
</Genres>
</BottomContentWrapper>
</DetailsWrapper>
{isCategorySelectOpen && (
<CategorySelect
open={isCategorySelectOpen}
onClose={(didUpdateCategories, addToCategories, removeFromCategories) => {
setIsCategorySelectOpen(false);
if (didUpdateCategories) {
addToLibrary(addToCategories, removeFromCategories);
}
}}
mangaId={manga.id}
addToLibrary
/>
)}
{CategorySelectComponent}
</>
);
};

View File

@@ -19,12 +19,12 @@ import {
useMediaQuery,
useTheme,
} from '@mui/material';
import React, { useState } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { CategorySelect } from '@/components/navbar/action/CategorySelect';
import { TManga } from '@/typings.ts';
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
interface IProps {
manga: TManga;
@@ -44,7 +44,9 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
setAnchorEl(null);
};
const [editCategories, setEditCategories] = useState(false);
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
mangaId: manga.id,
});
return (
<>
@@ -76,7 +78,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<Tooltip title={t('manga.label.edit_categories')}>
<IconButton
onClick={() => {
setEditCategories(true);
openCategorySelect(true);
}}
>
<Label />
@@ -134,7 +136,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<MenuItem
key="categories"
onClick={() => {
setEditCategories(true);
openCategorySelect(true);
handleClose();
}}
>
@@ -148,7 +150,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
</>
)}
<CategorySelect open={editCategories} onClose={() => setEditCategories(false)} mangaId={manga.id} />
{CategorySelectComponent}
</>
);
};

View File

@@ -0,0 +1,117 @@
/*
* 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, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/components/util/Toast.tsx';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { Categories } from '@/lib/data/Categories.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { Mangas } from '@/lib/data/Mangas.ts';
import { TManga } from '@/typings.ts';
import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx';
export const useManageMangaLibraryState = (manga: Pick<TManga, 'id' | 'title' | 'inLibrary'>) => {
const { t } = useTranslation();
const [isInLibrary, setIsInLibrary] = useState(manga.inLibrary);
const {
settings: { showAddToLibraryCategorySelectDialog },
loading: areSettingsLoading,
} = useMetadataServerSettings();
const categories = requestManager.useGetCategories();
const userCreatedCategories = useMemo(
() => Categories.getUserCreated(categories.data?.categories.nodes ?? []),
[categories.data?.categories.nodes],
);
const addToLibrary = useCallback(
(didSubmit: boolean, addToCategories: number[] = [], removeFromCategories: number[] = []) => {
if (!didSubmit) {
return;
}
requestManager
.updateManga(manga.id, {
updateManga: { inLibrary: true },
updateMangaCategories: { addToCategories, removeFromCategories },
})
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
.then(() => setIsInLibrary(true))
.catch(() => {
makeToast(t('library.error.label.add_to_library'), 'error');
});
},
[manga.id],
);
const removeFromLibrary = useCallback(async () => {
await awaitConfirmation({
title: t('global.label.are_you_sure'),
message: t('manga.action.library.remove.dialog.label.message', { title: manga.title }),
actions: {
confirm: { title: t('global.button.remove') },
},
});
await Mangas.removeFromLibrary([manga.id]);
setIsInLibrary(false);
}, [manga.id]);
const { openCategorySelect, CategorySelectComponent } = useCategorySelect({
mangaId: manga.id,
addToLibrary: true,
onClose: addToLibrary,
});
const updateLibraryState = useCallback(() => {
if (isInLibrary) {
removeFromLibrary().catch(
defaultPromiseErrorHandler('useManageMangaLibraryState::updateLibraryState::removeFromLibrary'),
);
return;
}
if (areSettingsLoading || 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(true, Categories.getIds(Categories.getDefaults(userCreatedCategories!)));
return;
}
openCategorySelect(true);
}, [isInLibrary, removeFromLibrary, addToLibrary, areSettingsLoading, categories.loading]);
return {
CategorySelectComponent,
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<TManga>(manga.id)?.inLibrary ?? isInLibrary,
};
};