Move category files into new folder
This commit is contained in:
21
src/modules/category/Category.types.ts
Normal file
21
src/modules/category/Category.types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryMetaType, CategoryType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export interface ICategoryMetadata extends LibraryOptions {}
|
||||
|
||||
export type CategoryMetadataKeys = keyof ICategoryMetadata;
|
||||
|
||||
export type CategoryIdInfo = Pick<CategoryType, 'id'>;
|
||||
export type CategoryNameInfo = Pick<CategoryType, 'name'>;
|
||||
export type CategoryDefaultInfo = Pick<CategoryType, 'default'>;
|
||||
export type CategoryUpdateInclusionInfo = Pick<CategoryType, 'includeInUpdate'>;
|
||||
export type CategoryDownloadInclusionInfo = Pick<CategoryType, 'includeInDownload'>;
|
||||
export type CategoryMetadataInfo = CategoryIdInfo & { meta: Pick<CategoryMetaType, 'key' | 'value'>[] };
|
||||
218
src/modules/category/components/CategoriesInclusionSetting.tsx
Normal file
218
src/modules/category/components/CategoriesInclusionSetting.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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 { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { t as translate } from 'i18next';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { CheckboxContainer } from '@/modules/core/components/inputs/CheckboxContainer.ts';
|
||||
import {
|
||||
CategoryDownloadInclusionInfo,
|
||||
CategoryIdInfo,
|
||||
CategoryNameInfo,
|
||||
CategoryUpdateInclusionInfo,
|
||||
} from '@/modules/category/Category.types.ts';
|
||||
|
||||
type CategoryType = CategoryIdInfo & CategoryNameInfo & CategoryUpdateInclusionInfo & CategoryDownloadInclusionInfo;
|
||||
|
||||
const booleanToIncludeOrExcludeStatus = (status: boolean | null | undefined): IncludeOrExclude => {
|
||||
switch (status) {
|
||||
case false:
|
||||
return IncludeOrExclude.Exclude;
|
||||
case true:
|
||||
return IncludeOrExclude.Include;
|
||||
case null:
|
||||
case undefined:
|
||||
return IncludeOrExclude.Unset;
|
||||
default:
|
||||
throw new Error(`booleanToIncludeInStatus: unexpected IncludeOrExclude status "${status}"`);
|
||||
}
|
||||
};
|
||||
|
||||
const includeInUpdateStatusToBoolean = (status: IncludeOrExclude): boolean | null => {
|
||||
switch (status) {
|
||||
case IncludeOrExclude.Exclude:
|
||||
return false;
|
||||
case IncludeOrExclude.Include:
|
||||
return true;
|
||||
case IncludeOrExclude.Unset:
|
||||
return null;
|
||||
default:
|
||||
throw new Error(`includeInUpdateStatusToBoolean: unexpected IncludeOrExclude status "${status}"`);
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryUpdateInfo = (
|
||||
categories: CategoryType[],
|
||||
areIncluded: boolean,
|
||||
unsetCategories: number,
|
||||
allCategories: number,
|
||||
) => {
|
||||
const noSpecificallyIncludedCategories = areIncluded && !categories.length && unsetCategories;
|
||||
const includesAllCategories = categories.length === allCategories;
|
||||
if (noSpecificallyIncludedCategories || includesAllCategories) {
|
||||
return translate('extension.language.all');
|
||||
}
|
||||
|
||||
if (!categories.length) {
|
||||
return translate('global.label.none');
|
||||
}
|
||||
|
||||
return categories.map((category) => category.name).join(', ');
|
||||
};
|
||||
|
||||
type CategoryIncludeField = keyof Pick<CategoryType, 'includeInUpdate' | 'includeInDownload'>;
|
||||
|
||||
export type CategoriesInclusionSettingProps = {
|
||||
categories: CategoryType[];
|
||||
includeField: CategoryIncludeField;
|
||||
dialogText?: string;
|
||||
};
|
||||
|
||||
export const CategoriesInclusionSetting = ({
|
||||
categories,
|
||||
includeField,
|
||||
dialogText,
|
||||
}: CategoriesInclusionSettingProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [dialogCategories, setDialogCategories] = useState(categories);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!categories) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogCategories(categories);
|
||||
}, [categories]);
|
||||
|
||||
const unsetCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Unset);
|
||||
const excludedCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Exclude);
|
||||
const includedCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Include);
|
||||
const excludedCategoriesText = getCategoryUpdateInfo(
|
||||
excludedCategories,
|
||||
false,
|
||||
unsetCategories.length,
|
||||
categories.length,
|
||||
);
|
||||
const includedCategoriesText = getCategoryUpdateInfo(
|
||||
includedCategories,
|
||||
true,
|
||||
unsetCategories.length,
|
||||
categories.length,
|
||||
);
|
||||
|
||||
const updateCategory = (category: CategoryType) =>
|
||||
requestManager.updateCategory(category.id, { [includeField]: category[includeField] }).response;
|
||||
|
||||
const updateCategories = async () => {
|
||||
const categoriesToUpdate = dialogCategories.filter((category) => {
|
||||
const currentCategory = categories?.find((currCategory) => currCategory.id === category.id);
|
||||
|
||||
if (!currentCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentCategory[includeField] !== category[includeField];
|
||||
});
|
||||
|
||||
setIsDialogOpen(false);
|
||||
|
||||
try {
|
||||
await Promise.all(categoriesToUpdate.map((category) => updateCategory(category)));
|
||||
// TODO - update cache immediately
|
||||
// mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false });
|
||||
} catch (error) {
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error');
|
||||
// mutate(categoriesEndpoint, [...categories]);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialogCategories(categories);
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={t('category.title.category_other')}
|
||||
secondary={
|
||||
<>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.include', {
|
||||
includedCategoriesText,
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.exclude', {
|
||||
excludedCategoriesText,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
||||
<DialogContent>
|
||||
<DialogTitle sx={{ paddingLeft: 0 }}>{t('category.title.category_other')}</DialogTitle>
|
||||
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
|
||||
<CheckboxContainer>
|
||||
{dialogCategories.map((category) => (
|
||||
<ThreeStateCheckboxInput
|
||||
key={category.id}
|
||||
label={category.name}
|
||||
checked={includeInUpdateStatusToBoolean(category[includeField])}
|
||||
onChange={(checked) => {
|
||||
const newIncludeState = booleanToIncludeOrExcludeStatus(checked);
|
||||
|
||||
const categoryIndex = dialogCategories.findIndex(
|
||||
(category_) => category_ === category,
|
||||
);
|
||||
const updatedDialogCategories = [
|
||||
...dialogCategories.slice(0, categoryIndex),
|
||||
{
|
||||
...category,
|
||||
[includeField]: newIncludeState,
|
||||
},
|
||||
...dialogCategories.slice(categoryIndex + 1, dialogCategories.length),
|
||||
];
|
||||
|
||||
setDialogCategories(updatedDialogCategories);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CheckboxContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDialog} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={updateCategories} color="primary">
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
252
src/modules/category/components/CategorySelect.tsx
Normal file
252
src/modules/category/components/CategorySelect.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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 { useState, useEffect, useMemo } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
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/Stack';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { Mangas } from '@/modules/manga/services/Mangas.ts';
|
||||
import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { Categories } from '@/modules/category/services/Categories.ts';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { updateMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import {
|
||||
GetCategoriesBaseQuery,
|
||||
GetCategoriesBaseQueryVariables,
|
||||
GetMangaCategoriesQuery,
|
||||
GetMangaCategoriesQueryVariables,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts';
|
||||
import { GET_MANGA_CATEGORIES } from '@/lib/graphql/queries/MangaQuery.ts';
|
||||
|
||||
type BaseProps = {
|
||||
open: boolean;
|
||||
onClose: (didUpdateCategories: boolean, addToCategories?: number[], removeFromCategories?: number[]) => void;
|
||||
};
|
||||
|
||||
type SingleMangaModeProps = {
|
||||
mangaId: number;
|
||||
addToLibrary?: boolean;
|
||||
};
|
||||
|
||||
type MultiMangaModeProps = {
|
||||
mangaIds: number[];
|
||||
};
|
||||
|
||||
export type CategorySelectProps =
|
||||
| (BaseProps & SingleMangaModeProps & PropertiesNever<MultiMangaModeProps>)
|
||||
| (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps);
|
||||
|
||||
const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => {
|
||||
const { data: mangaResult } = requestManager.useGetManga<GetMangaCategoriesQuery, GetMangaCategoriesQueryVariables>(
|
||||
GET_MANGA_CATEGORIES,
|
||||
mangaId ?? -1,
|
||||
{ skip: mangaId === undefined },
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (mangaId === undefined || !mangaResult) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Categories.getIds(mangaResult.manga.categories.nodes);
|
||||
}, [mangaResult?.manga.categories.nodes, mangaId]);
|
||||
};
|
||||
|
||||
const getCategoryCheckedState = (
|
||||
categoryId: number,
|
||||
categoriesToAdd: number[],
|
||||
categoriesToRemove: number[],
|
||||
isSingleSelectionMode: boolean,
|
||||
): boolean | undefined => {
|
||||
if (categoriesToAdd.includes(categoryId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isSingleSelectionMode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (categoriesToRemove.includes(categoryId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export function CategorySelect(props: CategorySelectProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { open, onClose, mangaId, mangaIds: passedMangaIds, addToLibrary = false } = props;
|
||||
|
||||
const isSingleSelectionMode = mangaId !== undefined;
|
||||
const mangaIds = passedMangaIds ?? [mangaId];
|
||||
|
||||
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
|
||||
|
||||
const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
|
||||
|
||||
const { data } = requestManager.useGetCategories<GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables>(
|
||||
GET_CATEGORIES_BASE,
|
||||
);
|
||||
const categoriesData = data?.categories.nodes;
|
||||
|
||||
const allCategories = useMemo(() => Categories.getUserCreated(categoriesData ?? []), [categoriesData]);
|
||||
|
||||
const defaultCategoryIds = useMemo(
|
||||
() => (addToLibrary ? Categories.getIds(Categories.getDefaults(allCategories)) : []),
|
||||
[allCategories],
|
||||
);
|
||||
|
||||
const { handleSelection, setSelectionForKey, getSelectionForKey } = useSelectableCollection<
|
||||
number,
|
||||
'categoriesToAdd' | 'categoriesToRemove'
|
||||
>(allCategories.length, {
|
||||
currentKey: 'categoriesToAdd',
|
||||
initialState: {
|
||||
categoriesToAdd: [...mangaCategoryIds, ...defaultCategoryIds],
|
||||
categoriesToRemove: [],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSelectionForKey('categoriesToAdd', [...mangaCategoryIds, ...defaultCategoryIds]);
|
||||
setSelectionForKey('categoriesToRemove', []);
|
||||
}, [mangaCategoryIds]);
|
||||
|
||||
const categoriesToAdd = getSelectionForKey('categoriesToAdd');
|
||||
const categoriesToRemove = getSelectionForKey('categoriesToRemove');
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectionForKey('categoriesToAdd', mangaCategoryIds);
|
||||
setSelectionForKey('categoriesToRemove', []);
|
||||
onClose(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
const addToCategories = isSingleSelectionMode
|
||||
? categoriesToAdd.filter((categoryId) => !mangaCategoryIds.includes(categoryId))
|
||||
: categoriesToAdd;
|
||||
const removeFromCategories = isSingleSelectionMode
|
||||
? mangaCategoryIds.filter((categoryId) => !categoriesToAdd.includes(categoryId))
|
||||
: categoriesToRemove;
|
||||
|
||||
onClose(true, addToCategories, removeFromCategories);
|
||||
|
||||
if (doNotShowAddToLibraryDialogAgain) {
|
||||
updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch(() =>
|
||||
makeToast(t('search.error.label.failed_to_save_settings'), 'error'),
|
||||
);
|
||||
}
|
||||
|
||||
const isUpdateRequired = !!addToCategories.length || !!removeFromCategories.length;
|
||||
if (!isUpdateRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addToLibrary) {
|
||||
// categories get updated in MangaDetails
|
||||
return;
|
||||
}
|
||||
|
||||
Mangas.performAction('change_categories', mangaIds, {
|
||||
changeCategoriesPatch: {
|
||||
addToCategories,
|
||||
removeFromCategories,
|
||||
},
|
||||
}).catch(defaultPromiseErrorHandler('CategorySelect::handleOk'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={{
|
||||
'.MuiDialog-paper': {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
|
||||
{allCategories.map((category) => (
|
||||
<ThreeStateCheckboxInput
|
||||
checked={getCategoryCheckedState(
|
||||
category.id,
|
||||
categoriesToAdd,
|
||||
categoriesToRemove,
|
||||
isSingleSelectionMode,
|
||||
)}
|
||||
onChange={(checked) => {
|
||||
handleSelection(category.id, false, { key: 'categoriesToAdd' });
|
||||
handleSelection(category.id, false, { key: 'categoriesToRemove' });
|
||||
|
||||
if (checked) {
|
||||
handleSelection(category.id, true, { key: 'categoriesToAdd' });
|
||||
}
|
||||
|
||||
if (checked === false) {
|
||||
handleSelection(category.id, true, { key: 'categoriesToRemove' });
|
||||
}
|
||||
}}
|
||||
label={category.name}
|
||||
key={category.id}
|
||||
/>
|
||||
))}
|
||||
</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
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
74
src/modules/category/components/CategorySettingsCard.tsx
Normal file
74
src/modules/category/components/CategorySettingsCard.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { DraggableProvided } from 'react-beautiful-dnd';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { CategoryType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export const CategorySettingsCard = ({
|
||||
category,
|
||||
provided,
|
||||
onEdit,
|
||||
}: {
|
||||
category: Pick<CategoryType, 'id' | 'name'>;
|
||||
provided: DraggableProvided;
|
||||
onEdit: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const deleteCategory = () => {
|
||||
requestManager.deleteCategory(category.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1, pb: 0 }} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
||||
<Card>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 1.5,
|
||||
'&:last-child': {
|
||||
paddingBottom: 1.5,
|
||||
},
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<DragHandleIcon />
|
||||
<Typography sx={{ flexGrow: 1 }} variant="h6" component="h2">
|
||||
{category.name}
|
||||
</Typography>
|
||||
<Stack sx={{ flexDirection: 'row' }}>
|
||||
<Tooltip title={t('global.button.edit')}>
|
||||
<IconButton component={Box} onClick={onEdit} size="large">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('chapter.action.download.delete.label.action')}>
|
||||
<IconButton component={Box} onClick={deleteCategory} size="large">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
43
src/modules/category/hooks/useCategorySelect.tsx
Normal file
43
src/modules/category/hooks/useCategorySelect.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 '@/modules/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,
|
||||
};
|
||||
};
|
||||
202
src/modules/category/screens/CategorySettings.tsx
Normal file
202
src/modules/category/screens/CategorySettings.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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 { useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Fab from '@mui/material/Fab';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { StrictModeDroppable } from '@/modules/core/components/StrictModeDroppable.tsx';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { GetCategoriesSettingsQuery, GetCategoriesSettingsQueryVariables } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_CATEGORIES_SETTINGS } from '@/lib/graphql/queries/CategoryQuery.ts';
|
||||
import { CategorySettingsCard } from '@/modules/category/components/CategorySettingsCard.tsx';
|
||||
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||
|
||||
export function CategorySettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { setTitle, setAction } = useContext(NavBarContext);
|
||||
useLayoutEffect(() => {
|
||||
setTitle(t('category.title.category_other'));
|
||||
setAction(null);
|
||||
|
||||
return () => {
|
||||
setTitle('');
|
||||
setAction(null);
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
const { data, loading, error, refetch } = requestManager.useGetCategories<
|
||||
GetCategoriesSettingsQuery,
|
||||
GetCategoriesSettingsQueryVariables
|
||||
>(GET_CATEGORIES_SETTINGS, { notifyOnNetworkStatusChange: true });
|
||||
const categories = useMemo(() => {
|
||||
const res = [...(data?.categories.nodes ?? [])];
|
||||
if (res.length > 0 && res[0].name === 'Default') {
|
||||
res.shift();
|
||||
}
|
||||
return res;
|
||||
}, [data]);
|
||||
|
||||
const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [dialogName, setDialogName] = useState<string>('');
|
||||
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
|
||||
const [reorderCategory, { reset: revertReorder }] = requestManager.useReorderCategory();
|
||||
const theme = useTheme();
|
||||
|
||||
const categoryReorder = (list: CategoryIdInfo[], from: number, to: number) => {
|
||||
const reorderedCategory = list[from];
|
||||
|
||||
reorderCategory({ variables: { input: { id: reorderedCategory.id, position: to + 1 } } }).catch(() =>
|
||||
revertReorder(),
|
||||
);
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
// dropped outside the list?
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
categoryReorder(categories, result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
const resetDialog = () => {
|
||||
setDialogName('');
|
||||
setDialogDefault(false);
|
||||
setCategoryToEdit(-1);
|
||||
};
|
||||
|
||||
const handleDialogOpen = () => {
|
||||
resetDialog();
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogOpen = (index: number) => {
|
||||
setDialogName(categories[index].name);
|
||||
setDialogDefault(categories[index].default);
|
||||
setCategoryToEdit(index);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDialogCancel = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDialogSubmit = () => {
|
||||
setDialogOpen(false);
|
||||
|
||||
if (categoryToEdit === -1) {
|
||||
requestManager.createCategory({ name: dialogName, default: dialogDefault });
|
||||
} else {
|
||||
const category = categories[categoryToEdit];
|
||||
requestManager.updateCategory(category.id, { name: dialogName, default: dialogDefault });
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('category.error.label.request_failure')}
|
||||
messageExtra={error.message}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('CategorySettings::refetch'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<StrictModeDroppable droppableId="droppable">
|
||||
{(droppableProvided) => (
|
||||
<Box ref={droppableProvided.innerRef} sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }}>
|
||||
{categories.map((category, index) => (
|
||||
<Draggable key={category.id} draggableId={category.id.toString()} index={index}>
|
||||
{(draggableProvided) => (
|
||||
<CategorySettingsCard
|
||||
provided={draggableProvided}
|
||||
category={category}
|
||||
onEdit={() => handleEditDialogOpen(index)}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{droppableProvided.placeholder}
|
||||
</Box>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="add"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={handleDialogOpen}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{categoryToEdit === -1
|
||||
? t('category.dialog.title.new_category')
|
||||
: t('category.dialog.title.edit_category')}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
label={t('category.label.category_name')}
|
||||
type="text"
|
||||
fullWidth
|
||||
value={dialogName}
|
||||
onChange={(e) => setDialogName(e.target.value)}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={dialogDefault} onChange={(e) => setDialogDefault(e.target.checked)} />
|
||||
}
|
||||
label={t('category.label.use_as_default_category')}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogCancel} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmit} color="primary">
|
||||
{t('global.button.submit')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
src/modules/category/services/Categories.ts
Normal file
25
src/modules/category/services/Categories.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { CategoryDefaultInfo, CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||
|
||||
export const DEFAULT_CATEGORY_ID = 0;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
89
src/modules/category/services/CategoryMetadata.ts
Normal file
89
src/modules/category/services/CategoryMetadata.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 { AllowedMetadataValueTypes, AppMetadataKeys, GqlMetaHolder, Metadata } from '@/typings.ts';
|
||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||
import { convertFromGqlMeta, getMetadataFrom, requestUpdateCategoryMetadata } from '@/lib/metadata/metadata.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryIdInfo, CategoryMetadataKeys, ICategoryMetadata } from '@/modules/category/Category.types.ts';
|
||||
|
||||
export const getDefaultCategoryMetadata = (): ICategoryMetadata => ({
|
||||
// display options
|
||||
showContinueReadingButton: false,
|
||||
showDownloadBadge: false,
|
||||
showUnreadBadge: false,
|
||||
gridLayout: GridLayout.Compact,
|
||||
|
||||
// sort options
|
||||
sortDesc: undefined,
|
||||
sortBy: undefined,
|
||||
|
||||
// filter options
|
||||
hasDownloadedChapters: undefined,
|
||||
hasBookmarkedChapters: undefined,
|
||||
hasUnreadChapters: undefined,
|
||||
hasDuplicateChapters: undefined,
|
||||
hasTrackerBinding: {},
|
||||
hasStatus: {} as LibraryOptions['hasStatus'],
|
||||
});
|
||||
|
||||
const convertAppMetadataToGqlMetadata = (
|
||||
metadata: Partial<ICategoryMetadata>,
|
||||
): Metadata<string, AllowedMetadataValueTypes> => ({
|
||||
...metadata,
|
||||
hasTrackerBinding: metadata.hasTrackerBinding ? JSON.stringify(metadata.hasTrackerBinding) : undefined,
|
||||
hasStatus: metadata.hasStatus ? JSON.stringify(metadata.hasStatus) : undefined,
|
||||
});
|
||||
|
||||
const convertGqlMetadataToAppMetadata = (
|
||||
metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
|
||||
): ICategoryMetadata => ({
|
||||
...(metadata as unknown as ICategoryMetadata),
|
||||
hasTrackerBinding:
|
||||
jsonSaveParse<ICategoryMetadata['hasTrackerBinding']>(metadata.hasTrackerBinding as string) ??
|
||||
(undefined as any),
|
||||
hasStatus: jsonSaveParse<ICategoryMetadata['hasStatus']>(metadata.hasStatus as string) ?? (undefined as any),
|
||||
});
|
||||
|
||||
const getCategoryMetadataWithDefaultValueFallback = (
|
||||
meta?: Metadata,
|
||||
defaultMetadata: ICategoryMetadata = getDefaultCategoryMetadata(),
|
||||
applyMetadataMigration: boolean = true,
|
||||
): ICategoryMetadata =>
|
||||
convertGqlMetadataToAppMetadata(
|
||||
getMetadataFrom({ meta }, convertAppMetadataToGqlMetadata(defaultMetadata), applyMetadataMigration),
|
||||
);
|
||||
|
||||
export const getCategoryMetadata = (
|
||||
{ meta }: GqlMetaHolder = {},
|
||||
defaultMetadata?: ICategoryMetadata,
|
||||
applyMetadataMigration?: boolean,
|
||||
): ICategoryMetadata =>
|
||||
getCategoryMetadataWithDefaultValueFallback(convertFromGqlMeta(meta), defaultMetadata, applyMetadataMigration);
|
||||
|
||||
export const updateCategoryMetadata = async <
|
||||
MetadataKeys extends CategoryMetadataKeys = CategoryMetadataKeys,
|
||||
MetadataKey extends MetadataKeys = MetadataKeys,
|
||||
>(
|
||||
category: CategoryIdInfo & GqlMetaHolder,
|
||||
metadataKey: MetadataKey,
|
||||
value: ICategoryMetadata[MetadataKey],
|
||||
): Promise<void[]> =>
|
||||
requestUpdateCategoryMetadata(category, [
|
||||
[metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]],
|
||||
]);
|
||||
|
||||
export const createUpdateCategoryMetadata =
|
||||
<Settings extends CategoryMetadataKeys>(
|
||||
category: CategoryIdInfo & GqlMetaHolder,
|
||||
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateCategoryMetadata'),
|
||||
): ((...args: OmitFirst<Parameters<typeof updateCategoryMetadata<Settings>>>) => Promise<void | void[]>) =>
|
||||
(metadataKey, value) =>
|
||||
updateCategoryMetadata(category, metadataKey, value).catch(handleError);
|
||||
@@ -23,7 +23,8 @@ import { Progress } from '@/modules/core/components/Progress.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { dateTimeFormatter } from '@/util/DateHelper.ts';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
import { CategoryIdInfo } from '@/lib/data/Categories.ts';
|
||||
|
||||
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||
|
||||
const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => {
|
||||
if (!status) {
|
||||
|
||||
@@ -21,8 +21,7 @@ import { Trackers } from '@/modules/tracker/services/Trackers.ts';
|
||||
import { GetTrackersSettingsQuery, MangaStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import { statusToTranslationKey } from '@/modules/manga/services/Mangas.ts';
|
||||
import { CategoryMetadataInfo } from '@/lib/data/Categories.ts';
|
||||
import { createUpdateCategoryMetadata, getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { createUpdateCategoryMetadata, getCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
} from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
import { LibrarySortMode } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryMetadataInfo } from '@/modules/category/Category.types.ts';
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { getDefaultCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
|
||||
type ContextType = {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { LibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { getDefaultCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -12,10 +12,10 @@ import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings
|
||||
import { ChapterType, MangaType, SourceType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MangaChapterCountInfo, MangaIdInfo } from '@/modules/manga/services/Mangas.ts';
|
||||
import { enhancedCleanup } from '@/lib/data/Strings.ts';
|
||||
import { CategoryMetadataInfo } from '@/lib/data/Categories.ts';
|
||||
import { getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { getCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { NullAndUndefined } from '@/Base.types.ts';
|
||||
import { LibraryOptions, LibrarySortMode } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryMetadataInfo } from '@/modules/category/Category.types.ts';
|
||||
|
||||
const triStateFilter = (
|
||||
triState: NullAndUndefined<boolean>,
|
||||
|
||||
@@ -40,7 +40,7 @@ import { Mangas } from '@/modules/manga/services/Mangas.ts';
|
||||
import { MANGA_CHAPTER_STAT_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
|
||||
import { useLibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { getCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
|
||||
const TitleWithSizeTag = styled('span')({
|
||||
display: 'flex',
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from '@/modules/core/components/menu/Menu.utils.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { TrackManga } from '@/modules/tracker/components/TrackManga.tsx';
|
||||
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||
import { useCategorySelect } from '@/modules/category/hooks/useCategorySelect.tsx';
|
||||
import { ChaptersDownloadActionMenuItems } from '@/modules/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx';
|
||||
import { NestedMenuItem } from '@/modules/core/components/menu/NestedMenuItem.tsx';
|
||||
import { MangaChapterStatFieldsFragment, MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Link } from 'react-router-dom';
|
||||
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||
import { useCategorySelect } from '@/modules/category/hooks/useCategorySelect.tsx';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -10,11 +10,11 @@ import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import gql from 'graphql-tag';
|
||||
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||
import { useCategorySelect } from '@/modules/category/hooks/useCategorySelect.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { Categories } from '@/lib/data/Categories.ts';
|
||||
import { Categories } from '@/modules/category/services/Categories.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { Mangas } from '@/modules/manga/services/Mangas.ts';
|
||||
import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx';
|
||||
|
||||
Reference in New Issue
Block a user