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

239 lines
9.4 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/. */
import React, { useMemo, useState, useContext, useEffect } from 'react';
import { List, ListItem, ListItemText, ListItemIcon, IconButton } from '@mui/material';
2021-02-20 01:23:52 +03:30
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
DraggingStyle,
NotDraggingStyle,
2021-05-30 04:01:49 +04:30
} 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, Palette } from '@mui/material/styles';
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';
2021-10-30 16:10:34 +03:30
import NavbarContext from 'components/context/NavbarContext';
import client, { useQuery } from 'util/client';
import { ICategory } from 'typings';
import { useTranslation } from 'react-i18next';
import { DEFAULT_FULL_FAB_HEIGHT } from 'components/util/StyledFab';
2021-02-20 01:23:52 +03:30
const getItemStyle = (
isDragging: boolean,
draggableStyle: DraggingStyle | NotDraggingStyle | undefined,
palette: Palette,
) => ({
2021-02-20 01:23:52 +03:30
// styles we need to apply on draggables
...draggableStyle,
...(isDragging && {
2021-09-09 17:51:22 +04:30
background: palette.mode === 'dark' ? '#424242' : 'rgb(235,235,235)',
2021-02-20 01:23:52 +03:30
}),
});
export default function Categories() {
const { t } = useTranslation();
2021-03-09 16:44:09 +03:30
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => {
setTitle(t('category.title.categories'));
setAction(null);
}, [t]);
2021-03-09 16:44:09 +03:30
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
const categories = useMemo(() => {
const res = [...(data ?? [])];
if (res.length > 0 && res[0].name === 'Default') {
res.shift();
}
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);
2021-02-20 01:23:52 +03:30
const theme = useTheme();
2021-05-30 04:01:49 +04:30
const categoryReorder = (list: ICategory[], from: number, to: number) => {
const newData = [...list];
const [removed] = newData.splice(from, 1);
newData.splice(to, 0, removed);
mutate(newData, { revalidate: false });
2021-02-20 01:23:52 +03:30
const formData = new FormData();
2021-05-30 04:01:49 +04:30
formData.append('from', `${from + 1}`);
formData.append('to', `${to + 1}`);
client.patch('/api/v1/category/reorder', formData).finally(() => mutate());
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
const formData = new FormData();
formData.append('name', dialogName);
2021-05-30 04:01:49 +04:30
formData.append('default', dialogDefault.toString());
2021-02-20 01:23:52 +03:30
if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData).finally(() => mutate());
2021-02-20 01:23:52 +03:30
} else {
const category = categories[categoryToEdit];
client.patch(`/api/v1/category/${category.id}`, formData).finally(() => mutate());
2021-02-20 01:23:52 +03:30
}
};
const deleteCategory = (index: number) => {
2021-02-20 01:23:52 +03:30
const category = categories[index];
client.delete(`/api/v1/category/${category.id}`).finally(() => mutate());
2021-02-20 01:23:52 +03:30
};
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(droppableProvided) => (
<List ref={droppableProvided.innerRef} sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }}>
2021-02-20 01:23:52 +03:30
{categories.map((item, index) => (
<Draggable key={item.id} draggableId={item.id.toString()} index={index}>
{(draggableProvided, snapshot) => (
2021-02-20 01:23:52 +03:30
<ListItem
ContainerProps={{ ref: draggableProvided.innerRef } as any}
{...draggableProvided.draggableProps}
{...draggableProvided.dragHandleProps}
2021-02-20 01:23:52 +03:30
style={getItemStyle(
snapshot.isDragging,
draggableProvided.draggableProps.style,
2021-02-20 01:23:52 +03:30
theme.palette,
)}
ref={draggableProvided.innerRef}
2021-02-20 01:23:52 +03:30
>
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
<ListItemText primary={item.name} />
2021-02-20 01:23:52 +03:30
<IconButton
onClick={() => {
handleEditDialogOpen(index);
2021-02-20 01:23:52 +03:30
}}
2021-09-09 17:51:22 +04:30
size="large"
2021-02-20 01:23:52 +03:30
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteCategory(index);
}}
2021-09-09 17:51:22 +04:30
size="large"
2021-02-20 01:23:52 +03:30
>
<DeleteIcon />
</IconButton>
</ListItem>
)}
</Draggable>
))}
{droppableProvided.placeholder}
2021-02-20 01:23:52 +03:30
</List>
)}
</Droppable>
</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={
<Checkbox
checked={dialogDefault}
onChange={(e) => setDialogDefault(e.target.checked)}
color="default"
/>
}
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>
</>
);
}