From 9151034fbcc9bec8d2db39ea8bbcb0b3a1070f08 Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Sun, 21 Feb 2021 04:27:41 +0330 Subject: [PATCH] category done! --- .../main/kotlin/ir/armor/tachidesk/Main.kt | 7 ++ .../ir/armor/tachidesk/util/CategoryManga.kt | 11 ++ webUI/react/.gitignore | 1 + webUI/react/src/components/CategorySelect.tsx | 113 ++++++++++++++++++ webUI/react/src/components/MangaDetails.tsx | 29 ++++- webUI/react/src/screens/Library.tsx | 61 +++++++--- .../react/src/screens/settings/Categories.jsx | 4 + 7 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 webUI/react/src/components/CategorySelect.tsx diff --git a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt index 40371236..a262f20e 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt @@ -18,6 +18,7 @@ import ir.armor.tachidesk.util.getExtensionIcon import ir.armor.tachidesk.util.getExtensionList import ir.armor.tachidesk.util.getLibraryMangas import ir.armor.tachidesk.util.getManga +import ir.armor.tachidesk.util.getMangaCategories import ir.armor.tachidesk.util.getMangaList import ir.armor.tachidesk.util.getPageImage import ir.armor.tachidesk.util.getSource @@ -170,6 +171,12 @@ class Main { ctx.status(200) } + // adds the manga to category + app.get("api/v1/manga/:mangaId/category/") { ctx -> + val mangaId = ctx.pathParam("mangaId").toInt() + ctx.json(getMangaCategories(mangaId)) + } + // adds the manga to category app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/CategoryManga.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/CategoryManga.kt index 8a542a7c..01a47163 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/CategoryManga.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/CategoryManga.kt @@ -1,9 +1,12 @@ package ir.armor.tachidesk.util +import ir.armor.tachidesk.database.dataclass.CategoryDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.table.CategoryMangaTable +import ir.armor.tachidesk.database.table.CategoryTable import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.toDataClass +import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert @@ -48,3 +51,11 @@ fun getCategoryMangaList(categoryId: Int): List { } } } + +fun getMangaCategories(mangaId: Int): List { + return transaction { + CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map { + CategoryTable.toDataClass(it) + } + } +} diff --git a/webUI/react/.gitignore b/webUI/react/.gitignore index 24daa585..ac57da47 100644 --- a/webUI/react/.gitignore +++ b/webUI/react/.gitignore @@ -1,3 +1,4 @@ node_modules/ .eslintcache .vscode +.env diff --git a/webUI/react/src/components/CategorySelect.tsx b/webUI/react/src/components/CategorySelect.tsx new file mode 100644 index 00000000..36bc410d --- /dev/null +++ b/webUI/react/src/components/CategorySelect.tsx @@ -0,0 +1,113 @@ +/* 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, { useEffect, useState } from 'react'; +import { makeStyles, createStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import Dialog from '@material-ui/core/Dialog'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormGroup from '@material-ui/core/FormGroup'; + +const useStyles = makeStyles(() => createStyles({ + paper: { + maxHeight: 435, + width: '80%', + }, +})); + +interface IProps { + open: boolean + setOpen: (value: boolean) => void + mangaId: number +} + +interface ICategoryInfo { + category: ICategory + selected: boolean +} + +export default function CategorySelect(props: IProps) { + const classes = useStyles(); + const { open, setOpen, mangaId } = props; + const [categoryInfos, setCategoryInfos] = useState([]); + + const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack + const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack + + useEffect(() => { + let tmpCategoryInfos: ICategoryInfo[] = []; + fetch('http://127.0.0.1:4567/api/v1/category/') + .then((response) => response.json()) + .then((data: ICategory[]) => { + tmpCategoryInfos = data.map((category) => ({ category, selected: false })); + }) + .then(() => { + fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/`) + .then((response) => response.json()) + .then((data: ICategory[]) => { + data.forEach((category) => { + tmpCategoryInfos[category.order - 1].selected = true; + }); + setCategoryInfos(tmpCategoryInfos); + }); + }); + }, [updateTriggerHolder]); + + const handleCancel = () => { + setOpen(false); + }; + + const handleOk = () => { + setOpen(false); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleChange = (event: React.ChangeEvent, categoryId: number) => { + const { checked } = event.target as HTMLInputElement; + fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/${categoryId}`, { + method: checked ? 'GET' : 'DELETE', mode: 'cors', + }) + .then(() => triggerUpdate()); + }; + + return ( + + Set categories + + + {categoryInfos.map((categoryInfo) => ( + handleChange(e, categoryInfo.category.id)} + name="checkedB" + color="default" + /> + )} + label={categoryInfo.category.name} + /> + ))} + + + + + + + + + ); +} diff --git a/webUI/react/src/components/MangaDetails.tsx b/webUI/react/src/components/MangaDetails.tsx index 8e1cf1c3..a030a07c 100644 --- a/webUI/react/src/components/MangaDetails.tsx +++ b/webUI/react/src/components/MangaDetails.tsx @@ -2,18 +2,31 @@ * 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 { Button } from '@material-ui/core'; +import { Button, createStyles, makeStyles } from '@material-ui/core'; import React, { useState } from 'react'; +import CategorySelect from './CategorySelect'; + +const useStyles = makeStyles(() => createStyles({ + root: { + display: 'flex', + flexDirection: 'row-reverse', + '& button': { + marginLeft: 10, + }, + }, +})); interface IProps{ manga: IManga } export default function MangaDetails(props: IProps) { + const classes = useStyles(); const { manga } = props; const [inLibrary, setInLibrary] = useState( manga.inLibrary ? 'In Library' : 'Not In Library', ); + const [categoryDialogOpen, setCategoryDialogOpen] = useState(true); function addToLibrary() { setInLibrary('adding'); @@ -38,13 +51,21 @@ export default function MangaDetails(props: IProps) { } return ( - <> +

{manga && manga.title}

-
+
+ {inLibrary === 'In Library' + && } +
- + +
); } diff --git a/webUI/react/src/screens/Library.tsx b/webUI/react/src/screens/Library.tsx index 30e24fe1..6bd298e4 100644 --- a/webUI/react/src/screens/Library.tsx +++ b/webUI/react/src/screens/Library.tsx @@ -20,7 +20,7 @@ interface TabPanelProps { function TabPanel(props: TabPanelProps) { const { - children, value, index, ...other + children, value, index, } = props; return ( @@ -28,9 +28,6 @@ function TabPanel(props: TabPanelProps) { 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}
@@ -41,31 +38,57 @@ export default function Library() { const { setTitle } = useContext(NavBarTitle); const [tabs, setTabs] = useState([]); const [tabNum, setTabNum] = useState(0); - const [lastPageNum, setLastPageNum] = useState(1); + // a hack so MangaGrid doesn't stop working. I won't change it in case + // if I do manga pagination for library.. + const [lastPageNum, setLastPageNum] = useState(1); useEffect(() => { setTitle('Library'); }, []); + + // eslint-disable-next-line @typescript-eslint/no-shadow + const fetchAndSetMangas = (tabs: IMangaCategory[], tab: IMangaCategory, index: number) => { + fetch(`http://127.0.0.1:4567/api/v1/category/${tab.category.id}`) + .then((response) => response.json()) + .then((data: IManga[]) => { + const tabsClone = JSON.parse(JSON.stringify(tabs)); + tabsClone[index].mangas = data; + setTabs(tabsClone); // clone the object + }); + }; + + const handleTabChange = (newTab: number) => { + setTabNum(newTab); + tabs.forEach((tab, index) => { + if (tab.category.order === newTab && tab.mangas.length === 0) { + // mangas are empty, fetch the mangas + fetchAndSetMangas(tabs, tab, index); + } + }); + }; + useEffect(() => { - // eslint-disable-next-line no-var - var newTabs: IMangaCategory[] = []; fetch('http://127.0.0.1:4567/api/v1/library') .then((response) => response.json()) .then((data: IManga[]) => { // if some manga with no category exist, they will be added under a virtual category if (data.length > 0) { - newTabs = [ + return [ { category: { - name: 'Default', isLanding: true, order: 0, id: 0, + name: 'Default', isLanding: true, order: 0, id: -1, }, mangas: data, }, ]; // will set state on the next fetch } + + // no default category so the first tab is 1 + setTabNum(1); + return []; }) .then( - () => { + (newTabs: IMangaCategory[]) => { fetch('http://127.0.0.1:4567/api/v1/category') .then((response) => response.json()) .then((data: ICategory[]) => { @@ -73,20 +96,24 @@ export default function Library() { category, mangas: [] as IManga[], })); - setTabs([...newTabs, ...mangaCategories]); + const newNewTabs = [...newTabs, ...mangaCategories]; + setTabs(newNewTabs); + + // if no default category, we must fetch the first tab now... + // eslint-disable-next-line max-len + if (newTabs.length === 0) { fetchAndSetMangas(newNewTabs, newNewTabs[0], 0); } }); }, ); }, []); - // 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) => ()); + // eslint-disable-next-line max-len + const tabDefines = tabs.map((tab) => ()); - const tabBodies = tabs.map((tab, index) => ( - + const tabBodies = tabs.map((tab) => ( + handleTabChange(newTab)} indicatorColor="primary" textColor="primary" centered={!scrollableTabs} diff --git a/webUI/react/src/screens/settings/Categories.jsx b/webUI/react/src/screens/settings/Categories.jsx index fa92385e..ac36e530 100644 --- a/webUI/react/src/screens/settings/Categories.jsx +++ b/webUI/react/src/screens/settings/Categories.jsx @@ -66,6 +66,7 @@ export default function Categories() { formData.append('to', to + 1); fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, { method: 'PATCH', + mode: 'cors', body: formData, }).finally(() => triggerUpdate()); @@ -112,12 +113,14 @@ export default function Categories() { if (categoryToEdit === -1) { fetch('http://127.0.0.1:4567/api/v1/category/', { method: 'POST', + mode: 'cors', body: formData, }).finally(() => triggerUpdate()); } else { const category = categories[categoryToEdit]; fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, { method: 'PATCH', + mode: 'cors', body: formData, }).finally(() => triggerUpdate()); } @@ -127,6 +130,7 @@ export default function Categories() { const category = categories[index]; fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, { method: 'DELETE', + mode: 'cors', }).finally(() => triggerUpdate()); };