Feature/improve add to library category selection dialog (#631)

* Create "Categories" util class

* Only show add to library category selection in case categories exist

* Add edit categories button in select categories dialog

* Add option to disable add to library dialog to the dialog
This commit is contained in:
schroda
2024-03-02 15:23:12 +01:00
committed by GitHub
parent 92fe3eaaa6
commit eb233aef3d
4 changed files with 102 additions and 31 deletions

View File

@@ -21,6 +21,8 @@ import { useMetadataServerSettings } from '@/util/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';
const DetailsWrapper = styled('div')(({ theme }) => ({
width: '100%',
@@ -177,6 +179,12 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
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(() => {
@@ -195,7 +203,21 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
};
const handleAddToLibraryClick = () => {
if (!showAddToLibraryCategorySelectDialog) {
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();
return;
}
@@ -236,7 +258,7 @@ export const MangaDetails: React.FC<IProps> = ({ manga }) => {
<MangaButtonsContainer inLibrary={manga.inLibrary}>
<div>
<Button
disabled={areSettingsLoading}
disabled={areSettingsLoading || categories.loading}
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
onClick={manga.inLibrary ? removeFromLibrary : handleAddToLibraryClick}
size="large"

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useEffect, useMemo } from 'react';
import { useState, useEffect, useMemo } from 'react';
import Button from '@mui/material/Button';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
@@ -14,11 +14,17 @@ 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';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Mangas } from '@/lib/data/Mangas.ts';
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
import { TCategory } from '@/typings.ts';
import { Categories } from '@/lib/data/Categories.ts';
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.ts';
import { makeToast } from '@/components/util/Toast.tsx';
type BaseProps = {
open: boolean;
@@ -46,7 +52,7 @@ const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => {
return [];
}
return mangaResult.manga.categories.nodes.map((category) => category.id);
return Categories.getIds(mangaResult.manga.categories.nodes);
}, [mangaResult?.manga.categories.nodes, mangaId]);
};
@@ -71,9 +77,6 @@ const getCategoryCheckedState = (
return undefined;
};
const getDefaultCategoryIds = (categories: TCategory[]) =>
categories.filter(({ default: isDefault }) => isDefault).map(({ id }) => id);
export function CategorySelect(props: Props) {
const { t } = useTranslation();
@@ -82,20 +85,17 @@ export function CategorySelect(props: Props) {
const isSingleSelectionMode = mangaId !== undefined;
const mangaIds = passedMangaIds ?? [mangaId];
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
const { metadata: serverMetadata } = useMetadataServerSettings();
const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
const { data } = requestManager.useGetCategories();
const categoriesData = data?.categories.nodes;
const allCategories = useMemo(() => {
const cats = [...(categoriesData ?? [])]; // make copy
if (cats.length > 0 && cats[0].name === 'Default') {
cats.shift(); // remove first category if it is 'Default'
}
return cats;
}, [categoriesData]);
const allCategories = useMemo(() => Categories.getUserCreated(categoriesData ?? []), [categoriesData]);
const defaultCategoryIds = useMemo(
() => (addToLibrary ? getDefaultCategoryIds(allCategories) : []),
() => (addToLibrary ? Categories.getIds(Categories.getDefaults(allCategories)) : []),
[allCategories],
);
@@ -134,6 +134,12 @@ export function CategorySelect(props: Props) {
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
: categoriesToRemove;
if (doNotShowAddToLibraryDialogAgain) {
requestUpdateServerMetadata(convertToGqlMeta(serverMetadata)! ?? {}, [
['showAddToLibraryCategorySelectDialog', false],
]).catch(() => makeToast(t('search.error.label.failed_to_save_settings'), 'error'));
}
const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length;
if (!isUpdateRequired) {
return;
@@ -161,13 +167,7 @@ export function CategorySelect(props: Props) {
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
<DialogContent dividers>
<FormGroup>
{allCategories.length === 0 && (
<span>
{t('category.error.no_categories_found.label.info')}
<br />
{t('category.error.no_categories_found.label.hint')}
</span>
)}
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
{allCategories.map((category) => (
<ThreeStateCheckboxInput
checked={getCategoryCheckedState(
@@ -195,12 +195,31 @@ export function CategorySelect(props: Props) {
</FormGroup>
</DialogContent>
<DialogActions>
<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)}
/>
)}
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between" alignItems="end">
<Button component={Link} to="/settings/categories">
{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>
</DialogActions>
</Dialog>
);

View File

@@ -13,8 +13,7 @@
},
"no_categories_found": {
"label": {
"hint": "You should make some from the settings.",
"info": "No categories found."
"info": "You don't have any categories yet."
}
}
},
@@ -296,7 +295,9 @@
"cancel": "Cancel",
"clear": "Clear",
"copy": "Copy",
"create": "Create",
"deselect": "Deselect",
"dont_show_dialog_again": "Don't show this dialog again",
"edit": "Edit",
"filter": "Filter",
"latest": "Latest",
@@ -370,6 +371,7 @@
"general": "General",
"github": "GitHub",
"links": "Links",
"load_in_progress": "Still loading required data…",
"loading": "Loading…",
"never": "Never",
"none": "None",

View File

@@ -0,0 +1,28 @@
/*
* 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 { TCategory } from '@/typings.ts';
export const DEFAULT_CATEGORY_ID = 0;
export type CategoryIdInfo = Pick<TCategory, 'id'>;
export type CategoryDefaultInfo = Pick<TCategory, 'default'>;
export class Categories {
static getIds(categories: CategoryIdInfo[]): number[] {
return categories.map((category) => category.id);
}
static getUserCreated<Category extends CategoryIdInfo>(categories: Category[]): Category[] {
return categories.filter((category) => category.id !== DEFAULT_CATEGORY_ID);
}
static getDefaults<Category extends CategoryDefaultInfo>(categories: Category[]): Category[] {
return categories.filter((category) => category.default);
}
}