finished the category screen
This commit is contained in:
+11
-3
@@ -14,13 +14,15 @@ import NavBar from './components/NavBar';
|
||||
import Home from './screens/Home';
|
||||
import Sources from './screens/Sources';
|
||||
import Extensions from './screens/Extensions';
|
||||
import MangaList from './screens/MangaList';
|
||||
import SourceMangas from './screens/SourceMangas';
|
||||
import Manga from './screens/Manga';
|
||||
import Reader from './screens/Reader';
|
||||
import Search from './screens/SearchSingle';
|
||||
import NavBarTitle from './context/NavbarTitle';
|
||||
import DarkTheme from './context/DarkTheme';
|
||||
import Library from './screens/Library';
|
||||
import Settings from './screens/Settings';
|
||||
import Categories from './screens/settings/Categories';
|
||||
|
||||
export default function App() {
|
||||
const [title, setTitle] = useState<string>('Tachidesk');
|
||||
@@ -69,10 +71,10 @@ export default function App() {
|
||||
<Extensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<MangaList popular />
|
||||
<SourceMangas popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<MangaList popular={false} />
|
||||
<SourceMangas popular={false} />
|
||||
</Route>
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
@@ -86,6 +88,12 @@ export default function App() {
|
||||
<Route path="/library">
|
||||
<Library />
|
||||
</Route>
|
||||
<Route path="/settings/categories">
|
||||
<Categories />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<Settings />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
|
||||
@@ -60,6 +60,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<ListItemText primary="Sources" />
|
||||
</ListItem>
|
||||
</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' }}>
|
||||
<ListItem button key="Search">
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -2,13 +2,45 @@
|
||||
* 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 { Tab, Tabs } from '@material-ui/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import MangaGrid from '../components/MangaGrid';
|
||||
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 [mangas, setMangas] = useState<IManga[]>([]);
|
||||
const [tabs, setTabs] = useState<IMangaCategory[]>([]);
|
||||
const [tabNum, setTabNum] = useState<number>(0);
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,16 +51,74 @@ export default function MangaList() {
|
||||
fetch('http://127.0.0.1:4567/api/v1/library')
|
||||
.then((response) => response.json())
|
||||
.then((data: IManga[]) => {
|
||||
setMangas(data);
|
||||
if (data.length > 0) {
|
||||
setTabs([
|
||||
...tabs,
|
||||
{
|
||||
category: {
|
||||
name: 'Default', isLanding: true, order: 0, id: 0,
|
||||
},
|
||||
mangas: data,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
}, [lastPageNum]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MangaGrid
|
||||
mangas={mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
/>
|
||||
);
|
||||
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
|
||||
mangas={mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return toRender;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom';
|
||||
import MangaGrid from '../components/MangaGrid';
|
||||
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 { setTitle } = useContext(NavBarTitle);
|
||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
Vendored
+7
@@ -38,3 +38,10 @@ interface IChapter {
|
||||
mangaId: number
|
||||
pageCount: number
|
||||
}
|
||||
|
||||
interface ICategory {
|
||||
id: number
|
||||
order: number
|
||||
name: String
|
||||
isLanding: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user