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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
28
src/lib/data/Categories.ts
Normal file
28
src/lib/data/Categories.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user