Files
suwayomi-material-you-webui/src/screens/settings/Categories.tsx

266 lines
10 KiB
TypeScript
Raw Normal View History

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
2021-02-20 01:23:52 +03:30
* 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/.
*/
2021-02-20 01:23:52 +03:30
import { useMemo, useState, useContext, useEffect } from 'react';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { DragDropContext, Draggable, DraggableProvided, DropResult } from 'react-beautiful-dnd';
2021-09-09 17:51:22 +04:30
import DragHandleIcon from '@mui/icons-material/DragHandle';
import EditIcon from '@mui/icons-material/Edit';
import { useTheme } from '@mui/material/styles';
2021-09-09 17:51:22 +04:30
import Fab from '@mui/material/Fab';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
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 Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
2023-10-28 00:32:02 +02:00
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { StrictModeDroppable } from '@/lib/StrictModeDroppable';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
import { NavBarContext } from '@/components/context/NavbarContext';
2024-04-27 16:24:55 +02:00
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
2024-04-29 15:29:33 +02:00
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
2024-04-27 22:28:55 +02:00
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import {
CategoryType,
GetCategoriesSettingsQuery,
GetCategoriesSettingsQueryVariables,
} from '@/lib/graphql/generated/graphql.ts';
import { GET_CATEGORIES_SETTINGS } from '@/lib/graphql/queries/CategoryQuery.ts';
import { CategoryIdInfo } from '@/lib/data/Categories.ts';
2021-02-20 01:23:52 +03:30
const CategoryCard = ({
category,
provided,
onEdit,
}: {
category: Pick<CategoryType, 'id' | 'name'>;
provided: DraggableProvided;
onEdit: () => void;
}) => {
const { t } = useTranslation();
2021-02-20 01:23:52 +03:30
const deleteCategory = () => {
requestManager.deleteCategory(category.id);
};
2021-02-20 01:23:52 +03:30
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>
);
};
2023-10-28 00:32:02 +02:00
export function Categories() {
const { t } = useTranslation();
2023-10-28 00:32:02 +02:00
const { setTitle, setAction } = useContext(NavBarContext);
useEffect(() => {
setTitle(t('category.title.category_other'));
setAction(null);
return () => {
setTitle('');
setAction(null);
};
}, [t]);
2021-03-09 16:44:09 +03:30
const { data, loading, error, refetch } = requestManager.useGetCategories<
GetCategoriesSettingsQuery,
GetCategoriesSettingsQueryVariables
>(GET_CATEGORIES_SETTINGS, { notifyOnNetworkStatusChange: true });
const categories = useMemo(() => {
2023-09-01 20:39:30 +02:00
const res = [...(data?.categories.nodes ?? [])];
if (res.length > 0 && res[0].name === 'Default') {
res.shift();
}
2023-10-15 16:03:08 +02:00
return res;
}, [data]);
2021-05-30 04:01:49 +04:30
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();
2021-02-20 01:23:52 +03:30
const theme = useTheme();
const categoryReorder = (list: CategoryIdInfo[], from: number, to: number) => {
2023-09-01 20:39:30 +02:00
const reorderedCategory = list[from];
reorderCategory({ variables: { input: { id: reorderedCategory.id, position: to + 1 } } }).catch(() =>
revertReorder(),
);
2021-02-20 01:23:52 +03:30
};
2021-05-30 04:01:49 +04:30
const onDragEnd = (result: DropResult) => {
2021-02-20 01:23:52 +03:30
// dropped outside the list?
if (!result.destination) {
return;
}
categoryReorder(categories, result.source.index, result.destination.index);
2021-02-20 01:23:52 +03:30
};
const resetDialog = () => {
setDialogName('');
setDialogDefault(false);
2021-02-20 01:23:52 +03:30
setCategoryToEdit(-1);
};
const handleDialogOpen = () => {
2021-02-20 01:23:52 +03:30
resetDialog();
setDialogOpen(true);
};
const handleEditDialogOpen = (index: number) => {
setDialogName(categories[index].name);
setDialogDefault(categories[index].default);
setCategoryToEdit(index);
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
2021-02-20 01:23:52 +03:30
};
const handleDialogSubmit = () => {
setDialogOpen(false);
2021-02-20 01:23:52 +03:30
if (categoryToEdit === -1) {
2023-09-01 20:39:30 +02:00
requestManager.createCategory({ name: dialogName, default: dialogDefault });
2021-02-20 01:23:52 +03:30
} else {
const category = categories[categoryToEdit];
2023-09-01 20:39:30 +02:00
requestManager.updateCategory(category.id, { name: dialogName, default: dialogDefault });
2021-02-20 01:23:52 +03:30
}
};
2024-04-27 16:24:55 +02:00
if (loading) {
return <LoadingPlaceholder />;
}
2024-04-27 22:28:55 +02:00
if (error) {
return (
2024-04-29 15:29:33 +02:00
<EmptyViewAbsoluteCentered
2024-04-27 22:28:55 +02:00
message={t('category.error.label.request_failure')}
messageExtra={error.message}
retry={() => refetch().catch(defaultPromiseErrorHandler('Categories::refetch'))}
/>
);
}
2021-02-20 01:23:52 +03:30
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) => (
<CategoryCard
provided={draggableProvided}
category={category}
onEdit={() => handleEditDialogOpen(index)}
/>
2021-02-20 01:23:52 +03:30
)}
</Draggable>
))}
{droppableProvided.placeholder}
</Box>
2021-02-20 01:23:52 +03:30
)}
</StrictModeDroppable>
2021-02-20 01:23:52 +03:30
</DragDropContext>
<Fab
color="primary"
aria-label="add"
style={{
position: 'fixed',
2021-02-20 01:23:52 +03:30
bottom: theme.spacing(2),
right: theme.spacing(2),
}}
onClick={handleDialogOpen}
>
<AddIcon />
</Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
2021-02-20 01:23:52 +03:30
<DialogTitle id="form-dialog-title">
{categoryToEdit === -1
? t('category.dialog.title.new_category')
: t('category.dialog.title.edit_category')}
2021-02-20 01:23:52 +03:30
</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label={t('category.label.category_name')}
2021-02-20 01:23:52 +03:30
type="text"
fullWidth
value={dialogName}
onChange={(e) => setDialogName(e.target.value)}
/>
<FormControlLabel
control={
2024-07-11 17:22:52 +02:00
<Checkbox checked={dialogDefault} onChange={(e) => setDialogDefault(e.target.checked)} />
}
label={t('category.label.use_as_default_category')}
2021-02-20 01:23:52 +03:30
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogCancel} color="primary">
{t('global.button.cancel')}
2021-02-20 01:23:52 +03:30
</Button>
<Button onClick={handleDialogSubmit} color="primary">
{t('global.button.submit')}
2021-02-20 01:23:52 +03:30
</Button>
</DialogActions>
</Dialog>
</>
);
}