finished the category screen

This commit is contained in:
Aria Moradi 2021-02-20 01:23:52 +03:30
parent 5a9d216fb7
commit f1cc37d0db
17 changed files with 516 additions and 41 deletions

View File

@ -29,6 +29,7 @@ import ir.armor.tachidesk.util.removeCategory
import ir.armor.tachidesk.util.removeExtension import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.removeMangaFromCategory import ir.armor.tachidesk.util.removeMangaFromCategory
import ir.armor.tachidesk.util.removeMangaFromLibrary import ir.armor.tachidesk.util.removeMangaFromLibrary
import ir.armor.tachidesk.util.reorderCategory
import ir.armor.tachidesk.util.sourceFilters import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch import ir.armor.tachidesk.util.sourceSearch
@ -60,7 +61,7 @@ class Main {
// make sure everything we need exists // make sure everything we need exists
applicationSetup() applicationSetup()
val tray = systemTray() val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
registerConfigModules() registerConfigModules()
@ -227,7 +228,7 @@ class Main {
ctx.json(sourceFilters(sourceId)) ctx.json(sourceFilters(sourceId))
} }
// lists all manga in the library, suitable if no categories are defined // lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx -> app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas()) ctx.json(getLibraryMangas())
} }
@ -240,20 +241,28 @@ class Main {
// category create // category create
app.post("/api/v1/category/") { ctx -> app.post("/api/v1/category/") { ctx ->
val name = ctx.formParam("name")!! val name = ctx.formParam("name")!!
val isLanding = ctx.formParam("isLanding", "false").toBoolean() createCategory(name)
createCategory(name, isLanding)
ctx.status(200) ctx.status(200)
} }
// category modification // category modification
app.put("/api/v1/category/:categoryId") { ctx -> app.patch("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId")!!.toInt()
val name = ctx.formParam("name")!! val name = ctx.formParam("name")
val isLanding = ctx.formParam("isLanding").toBoolean() val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
updateCategory(categoryId, name, isLanding) updateCategory(categoryId, name, isLanding)
ctx.status(200) ctx.status(200)
} }
// category re-ordering
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt()
reorderCategory(categoryId, from, to)
ctx.status(200)
}
// category delete // category delete
app.delete("/api/v1/category/:categoryId") { ctx -> app.delete("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt() val categoryId = ctx.pathParam("categoryId").toInt()

View File

@ -29,12 +29,14 @@ fun makeDataBaseTables() {
// db.useNestedTransactions = true // db.useNestedTransactions = true
transaction { transaction {
SchemaUtils.create(ExtensionTable) SchemaUtils.createMissingTablesAndColumns(
SchemaUtils.create(SourceTable) ExtensionTable,
SchemaUtils.create(MangaTable) SourceTable,
SchemaUtils.create(ChapterTable) MangaTable,
SchemaUtils.create(PageTable) ChapterTable,
SchemaUtils.create(CategoryTable) PageTable,
SchemaUtils.create(CategoryMangaTable) CategoryTable,
CategoryMangaTable,
)
} }
} }

View File

@ -5,6 +5,8 @@ package ir.armor.tachidesk.database.dataclass
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class CategoryDataClass( data class CategoryDataClass(
val id: Int,
val order: Int,
val name: String, val name: String,
val isLanding: Boolean val isLanding: Boolean
) )

View File

@ -11,9 +11,12 @@ import org.jetbrains.exposed.sql.ResultRow
object CategoryTable : IntIdTable() { object CategoryTable : IntIdTable() {
val name = varchar("name", 64) val name = varchar("name", 64)
val isLanding = bool("is_landing").default(false) val isLanding = bool("is_landing").default(false)
val order = integer("order").default(0)
} }
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass( fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
categoryEntry[CategoryTable.id].value,
categoryEntry[CategoryTable.order],
categoryEntry[CategoryTable.name], categoryEntry[CategoryTable.name],
categoryEntry[CategoryTable.isLanding], categoryEntry[CategoryTable.isLanding],
) )

View File

@ -25,6 +25,7 @@ object MangaTable : IntIdTable() {
val thumbnail_url = varchar("thumbnail_url", 2048).nullable() val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
val inLibrary = bool("in_library").default(false) val inLibrary = bool("in_library").default(false)
val defaultCategory = bool("default_category").default(true)
// source is used by some ancestor of IntIdTable // source is used by some ancestor of IntIdTable
val sourceReference = reference("source", SourceTable) val sourceReference = reference("source", SourceTable)

View File

@ -3,6 +3,7 @@ package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.CategoryDataClass import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryTable import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.toDataClass import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@ -14,21 +15,34 @@ import org.jetbrains.exposed.sql.update
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
fun createCategory(name: String, isLanding: Boolean) { fun createCategory(name: String) {
transaction { transaction {
val count = CategoryTable.selectAll().count()
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null) if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
CategoryTable.insert { CategoryTable.insert {
it[CategoryTable.name] = name it[CategoryTable.name] = name
it[CategoryTable.isLanding] = isLanding it[CategoryTable.order] = count.toInt() + 1
} }
} }
} }
fun updateCategory(categoryId: Int, name: String, isLanding: Boolean) { fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
transaction { transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) { CategoryTable.update({ CategoryTable.id eq categoryId }) {
it[CategoryTable.name] = name if (name != null) it[CategoryTable.name] = name
it[CategoryTable.isLanding] = isLanding if (isLanding != null) it[CategoryTable.isLanding] = isLanding
}
}
}
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
it[CategoryTable.order] = index + 1
}
} }
} }
} }
@ -41,7 +55,7 @@ fun removeCategory(categoryId: Int) {
fun getCategoryList(): List<CategoryDataClass> { fun getCategoryList(): List<CategoryDataClass> {
return transaction { return transaction {
CategoryTable.selectAll().map { CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it) CategoryTable.toDataClass(it)
} }
} }

View File

@ -9,6 +9,7 @@ import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* This Source Code Form is subject to the terms of the Mozilla Public /* 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
@ -21,6 +22,10 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
it[CategoryMangaTable.category] = categoryId it[CategoryMangaTable.category] = categoryId
it[CategoryMangaTable.manga] = mangaId it[CategoryMangaTable.manga] = mangaId
} }
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = false
}
} }
} }
} }
@ -28,6 +33,11 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) { fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
transaction { transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) } CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = true
}
}
} }
} }

View File

@ -3,6 +3,7 @@ package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
@ -35,7 +36,7 @@ fun removeMangaFromLibrary(mangaId: Int) {
fun getLibraryMangas(): List<MangaDataClass> { fun getLibraryMangas(): List<MangaDataClass> {
return transaction { return transaction {
MangaTable.select { MangaTable.inLibrary eq true }.map { MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it) MangaTable.toDataClass(it)
} }
} }

View File

@ -10,6 +10,7 @@
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"fontsource-roboto": "^4.0.0", "fontsource-roboto": "^4.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.1",

View File

@ -14,13 +14,15 @@ import NavBar from './components/NavBar';
import Home from './screens/Home'; import Home from './screens/Home';
import Sources from './screens/Sources'; import Sources from './screens/Sources';
import Extensions from './screens/Extensions'; import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList'; import SourceMangas from './screens/SourceMangas';
import Manga from './screens/Manga'; import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/SearchSingle'; import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle'; import NavBarTitle from './context/NavbarTitle';
import DarkTheme from './context/DarkTheme'; import DarkTheme from './context/DarkTheme';
import Library from './screens/Library'; import Library from './screens/Library';
import Settings from './screens/Settings';
import Categories from './screens/settings/Categories';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
@ -69,10 +71,10 @@ export default function App() {
<Extensions /> <Extensions />
</Route> </Route>
<Route path="/sources/:sourceId/popular/"> <Route path="/sources/:sourceId/popular/">
<MangaList popular /> <SourceMangas popular />
</Route> </Route>
<Route path="/sources/:sourceId/latest/"> <Route path="/sources/:sourceId/latest/">
<MangaList popular={false} /> <SourceMangas popular={false} />
</Route> </Route>
<Route path="/sources"> <Route path="/sources">
<Sources /> <Sources />
@ -86,6 +88,12 @@ export default function App() {
<Route path="/library"> <Route path="/library">
<Library /> <Library />
</Route> </Route>
<Route path="/settings/categories">
<Categories />
</Route>
<Route path="/settings">
<Settings />
</Route>
<Route path="/"> <Route path="/">
<Home /> <Home />
</Route> </Route>

View File

@ -60,6 +60,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" /> <ListItemText primary="Sources" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
</Link>
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}> {/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search"> <ListItem button key="Search">
<ListItemIcon> <ListItemIcon>

View File

@ -2,13 +2,45 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Tab, Tabs } from '@material-ui/core';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle'; import NavBarTitle from '../context/NavbarTitle';
export default function MangaList() { interface IMangaCategory {
category: ICategory
mangas: IManga[]
}
interface TabPanelProps {
children: React.ReactNode;
index: any;
value: any;
}
function TabPanel(props: TabPanelProps) {
const {
children, value, index, ...other
} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
// eslint-disable-next-line react/jsx-props-no-spreading
{...other}
>
{value === index && children}
</div>
);
}
export default function Library() {
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [tabs, setTabs] = useState<IMangaCategory[]>([]);
const [tabNum, setTabNum] = useState<number>(0);
const [lastPageNum, setLastPageNum] = useState<number>(1); const [lastPageNum, setLastPageNum] = useState<number>(1);
useEffect(() => { useEffect(() => {
@ -19,11 +51,66 @@ export default function MangaList() {
fetch('http://127.0.0.1:4567/api/v1/library') fetch('http://127.0.0.1:4567/api/v1/library')
.then((response) => response.json()) .then((response) => response.json())
.then((data: IManga[]) => { .then((data: IManga[]) => {
setMangas(data); if (data.length > 0) {
setTabs([
...tabs,
{
category: {
name: 'Default', isLanding: true, order: 0, id: 0,
},
mangas: data,
},
]);
}
}); });
}, [lastPageNum]); }, []);
return ( useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/category')
.then((response) => response.json())
.then((data: ICategory[]) => {
const mangaCategories = data.map((category) => ({
category,
mangas: [] as IManga[],
}));
setTabs([...tabs, ...mangaCategories]);
});
}, []);
// eslint-disable-next-line max-len
const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => setTabNum(newValue);
let toRender;
if (tabs.length > 1) {
const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} />));
const tabBodies = tabs.map((tab) => (
<TabPanel value={tabNum} index={0}>
<MangaGrid
mangas={tab.mangas}
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
</TabPanel>
));
toRender = (
<>
<Tabs
value={tabNum}
onChange={handleTabChange}
indicatorColor="primary"
textColor="primary"
centered
>
{tabDefines}
</Tabs>
{tabBodies}
</>
);
} else {
const mangas = tabs.length === 1 ? tabs[0].mangas : [];
toRender = (
<MangaGrid <MangaGrid
mangas={mangas} mangas={mangas}
hasNextPage={false} hasNextPage={false}
@ -32,3 +119,6 @@ export default function MangaList() {
/> />
); );
} }
return toRender;
}

View File

@ -0,0 +1,34 @@
/* 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 React, { useContext } from 'react';
import List from '@material-ui/core/List';
import ListItem, { ListItemProps } from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import InboxIcon from '@material-ui/icons/Inbox';
import NavBarTitle from '../context/NavbarTitle';
function ListItemLink(props: ListItemProps<'a', { button?: true }>) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ListItem button component="a" {...props} />;
}
export default function Settings() {
const { setTitle } = useContext(NavBarTitle);
setTitle('Settings');
return (
<div>
<List component="nav" style={{ padding: 0 }}>
<ListItemLink href="/settings/categories">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Categories" />
</ListItemLink>
</List>
</div>
);
}

View File

@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle'; import NavBarTitle from '../context/NavbarTitle';
export default function MangaList(props: { popular: boolean }) { export default function SourceMangas(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);

View File

@ -0,0 +1,231 @@
/* 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/. */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
import React, { useState, useContext, useEffect } from 'react';
import {
List,
ListItem,
ListItemText,
ListItemIcon,
IconButton,
} from '@material-ui/core';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import DragHandleIcon from '@material-ui/icons/DragHandle';
import EditIcon from '@material-ui/icons/Edit';
import { useTheme } from '@material-ui/core/styles';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
import DeleteIcon from '@material-ui/icons/Delete';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import NavBarTitle from '../../context/NavbarTitle';
const getItemStyle = (isDragging, draggableStyle, palette) => ({
// styles we need to apply on draggables
...draggableStyle,
...(isDragging && {
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
}),
});
export default function Categories() {
const { setTitle } = useContext(NavBarTitle);
setTitle('Categories');
const [categories, setCategories] = useState([]);
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = React.useState(false);
const [dialogValue, setDialogValue] = useState('');
const theme = useTheme();
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
if (!dialogOpen) {
fetch('http://127.0.0.1:4567/api/v1/category/')
.then((response) => response.json())
.then((data) => setCategories(data));
}
}, [updateTriggerHolder]);
const categoryReorder = (list, from, to) => {
const category = list[from];
const formData = new FormData();
formData.append('from', from + 1);
formData.append('to', to + 1);
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, {
method: 'PATCH',
body: formData,
}).finally(() => triggerUpdate());
// also move it in local state to avoid jarring moving behviour...
const result = Array.from(list);
const [removed] = result.splice(from, 1);
result.splice(to, 0, removed);
return result;
};
const onDragEnd = (result) => {
// dropped outside the list?
if (!result.destination) {
return;
}
setCategories(categoryReorder(
categories,
result.source.index,
result.destination.index,
));
};
const handleDialogOpen = () => {
setDialogOpen(true);
};
const resetDialog = () => {
setDialogOpen(false);
setDialogValue('');
setCategoryToEdit(-1);
};
const handleDialogCancel = () => {
resetDialog();
};
const handleDialogSubmit = () => {
resetDialog();
const formData = new FormData();
formData.append('name', dialogValue);
if (categoryToEdit === -1) {
fetch('http://127.0.0.1:4567/api/v1/category/', {
method: 'POST',
body: formData,
}).finally(() => triggerUpdate());
} else {
const category = categories[categoryToEdit];
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
method: 'PATCH',
body: formData,
}).finally(() => triggerUpdate());
}
};
const deleteCategory = (index) => {
const category = categories[index];
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
method: 'DELETE',
}).finally(() => triggerUpdate());
};
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<List ref={provided.innerRef}>
{categories.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id.toString()}
index={index}
>
{(provided, snapshot) => (
<ListItem
ContainerComponent="li"
ContainerProps={{ ref: provided.innerRef }}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
theme.palette,
)}
ref={provided.innerRef}
>
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
<ListItemText
primary={item.name}
/>
<IconButton
onClick={() => {
setCategoryToEdit(index);
handleDialogOpen();
}}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteCategory(index);
}}
>
<DeleteIcon />
</IconButton>
</ListItem>
)}
</Draggable>
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
<Fab
color="primary"
aria-label="add"
style={{
position: 'absolute',
bottom: theme.spacing(2),
right: theme.spacing(2),
}}
onClick={handleDialogOpen}
>
<AddIcon />
</Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
</DialogTitle>
<DialogContent>
<DialogContentText>
Enter new category name.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="Category Name"
type="text"
fullWidth
value={dialogValue}
onChange={(e) => setDialogValue(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogCancel} color="primary">
Cancel
</Button>
<Button onClick={handleDialogSubmit} color="primary">
Submit
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@ -38,3 +38,10 @@ interface IChapter {
mangaId: number mangaId: number
pageCount: number pageCount: number
} }
interface ICategory {
id: number
order: number
name: String
isLanding: boolean
}

View File

@ -3744,6 +3744,13 @@ css-blank-pseudo@^0.1.4:
dependencies: dependencies:
postcss "^7.0.5" postcss "^7.0.5"
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-color-names@0.0.4, css-color-names@^0.0.4: css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4" version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@ -7344,6 +7351,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memoize-one@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
memory-fs@^0.4.1: memory-fs@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@ -9208,6 +9220,11 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.4.1: raf@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@ -9257,6 +9274,19 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7" regenerator-runtime "^0.13.7"
whatwg-fetch "^3.4.1" whatwg-fetch "^3.4.1"
react-beautiful-dnd@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
dependencies:
"@babel/runtime" "^7.8.4"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.1.1"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-dev-utils@^11.0.1: react-dev-utils@^11.0.1:
version "11.0.1" version "11.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45"
@ -9301,7 +9331,7 @@ react-error-overlay@^6.0.8:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw== integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -9311,6 +9341,17 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
react-redux@^7.1.1:
version "7.2.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
dependencies:
"@babel/runtime" "^7.12.1"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.13.1"
react-refresh@^0.8.3: react-refresh@^0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@ -9518,6 +9559,14 @@ redent@^3.0.0:
indent-string "^4.0.0" indent-string "^4.0.0"
strip-indent "^3.0.0" strip-indent "^3.0.0"
redux@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^8.2.0: regenerate-unicode-properties@^8.2.0:
version "8.2.0" version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@ -10672,7 +10721,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1" unquote "~1.1.1"
util.promisify "~1.0.0" util.promisify "~1.0.0"
symbol-observable@1.2.0: symbol-observable@1.2.0, symbol-observable@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@ -10823,7 +10872,7 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2: tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
@ -11176,6 +11225,11 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
use-memo-one@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
use@^3.1.0: use@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"