Compare commits

...

10 Commits

Author SHA1 Message Date
Aria Moradi f41c5c9428 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-19 14:55:48 +03:30
Aria Moradi 04837983fa reader ui changes 2021-03-19 14:52:20 +03:30
Aria Moradi 5d484b012c new layout for manga page for >md 2021-03-18 22:55:17 +03:30
Aria Moradi 436a8d0585 improvments on the reader 2021-03-18 21:46:24 +03:30
Aria Moradi 28cc0a6f84 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-17 19:22:44 +03:30
Aria Moradi 26cc2f2c96 MangaDetails component improved drastically 2021-03-17 19:17:03 +03:30
Aria Moradi 149107e749 fix material error 2021-03-16 23:42:51 +03:30
Aria Moradi a74936c5f5 Update README.md 2021-03-16 23:24:14 +03:30
Aria Moradi ff8c8913d4 Update README.md 2021-03-16 23:14:36 +03:30
Aria Moradi 83426e1302 Update README.md 2021-03-16 23:13:44 +03:30
21 changed files with 784 additions and 235 deletions
+13 -6
View File
@@ -21,18 +21,25 @@ Here is a list of current features:
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
## Downloading and Running the app
### Downloading the app
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
### All Operating Systems
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
Download the latest jar release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
### Windows only
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`.
### Windows
Download the latest win32 release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
### Running on Docker
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
### Arch Linux
You can install Tachidesk from the AUR
```
yay -S tachidesk
```
### Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting
+1 -1
View File
@@ -9,7 +9,7 @@ plugins {
id("edu.sc.seis.launch4j") version "2.4.9"
}
val TachideskVersion = "v0.2.5"
val TachideskVersion = "v0.2.6"
repositories {
+2
View File
@@ -8,11 +8,13 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/react-lazyload": "^3.1.0",
"axios": "^0.21.1",
"fontsource-roboto": "^4.0.0",
"react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1",
"react-lazyload": "^3.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
+16 -3
View File
@@ -27,9 +27,12 @@ import useLocalStorage from './util/useLocalStorage';
export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />);
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = {
title, setTitle, action, setAction,
title, setTitle, action, setAction, override, setOverride,
};
const darkThemeContext = { darkTheme, setDarkTheme };
@@ -63,7 +66,12 @@ export default function App() {
<NavbarContext.Provider value={navBarContext}>
<CssBaseline />
<NavBar />
<Container maxWidth={false} disableGutters>
<Container
id="appMainContainer"
maxWidth={false}
disableGutters
style={{ paddingTop: '64px' }}
>
<Switch>
<Route path="/sources/:sourceId/search/">
<Search />
@@ -81,7 +89,7 @@ export default function App() {
<Sources />
</Route>
<Route path="/manga/:mangaId/chapter/:chapterId">
<Reader />
<></>
</Route>
<Route path="/manga/:id">
<Manga />
@@ -106,6 +114,11 @@ export default function App() {
/>
</Switch>
</Container>
<Switch>
<Route path="/manga/:mangaId/chapter/:chapterId">
<Reader />
</Route>
</Switch>
</NavbarContext.Provider>
</ThemeProvider>
</Router>
@@ -85,6 +85,14 @@ export default function CategorySelect(props: IProps) {
<DialogTitle>Set categories</DialogTitle>
<DialogContent dividers>
<FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => (
<FormControlLabel
control={(
+3 -1
View File
@@ -8,6 +8,7 @@ import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import { useHistory } from 'react-router-dom';
const useStyles = makeStyles((theme) => ({
root: {
@@ -41,6 +42,7 @@ interface IProps{
export default function ChapterCard(props: IProps) {
const classes = useStyles();
const history = useHistory();
const { chapter } = props;
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
@@ -64,7 +66,7 @@ export default function ChapterCard(props: IProps) {
</div>
</div>
<div style={{ display: 'flex' }}>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { history.push(`/manga/${chapter.mangaId}/chapter/${chapter.id}`); }}>open</Button>
</div>
</CardContent>
</Card>
+1 -1
View File
@@ -43,7 +43,7 @@ const useStyles = makeStyles({
});
interface IProps {
manga: IManga
manga: IMangaCard
}
const MangaCard = React.forwardRef((props: IProps, ref) => {
const {
+196 -33
View File
@@ -2,55 +2,180 @@
* 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, createStyles, makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import { Theme } from '@material-ui/core/styles';
import FavoriteIcon from '@material-ui/icons/Favorite';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'fixed',
width: '50vw',
},
},
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
leftRight: {
display: 'flex',
flexDirection: 'row-reverse',
},
leftSide: {
'& img': {
borderRadius: 4,
maxWidth: '100%',
minWidth: '100%',
height: 'auto',
},
maxWidth: '50%',
// [theme.breakpoints.up('md')]: {
// minWidth: '100px',
// },
},
rightSide: {
marginLeft: 15,
maxWidth: '100%',
'& span': {
fontWeight: '400',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
buttons: {
display: 'flex',
justifyContent: 'space-around',
'& button': {
marginLeft: 10,
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
},
'& span': {
display: 'block',
fontSize: '0.85em',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
},
},
},
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
},
},
}));
interface IProps{
manga: IManga
source: ISource
}
function getSourceName(source: ISource) {
if (source.name !== null) { return source.name; }
if (source.name !== null) {
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
}
return source.id;
}
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) {
const classes = useStyles();
const { manga, source } = props;
const { setAction } = useContext(NavbarContext);
const { manga } = props;
const [inLibrary, setInLibrary] = useState<string>(
manga.inLibrary ? 'In Library' : 'Not In Library',
manga.inLibrary ? 'In Library' : 'Add to Library',
);
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
useEffect(() => {
if (inLibrary === 'In Library') {
setAction(
<>
<IconButton
onClick={() => setCategoryDialogOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</>,
);
} else { setAction(<></>); }
}, [inLibrary, categoryDialogOpen]);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(inLibrary)();
function addToLibrary() {
setInLibrary('adding');
// setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library');
});
}
function removeFromLibrary() {
setInLibrary('removing');
// setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Not In Library');
setInLibrary('Add To Library');
});
}
function handleButtonClick() {
if (inLibrary === 'Not In Library') {
if (inLibrary === 'Add To Library') {
addToLibrary();
} else {
removeFromLibrary();
@@ -58,26 +183,64 @@ export default function MangaDetails(props: IProps) {
}
return (
<div>
<h1>
{manga.title}
</h1>
<h3>
Source:
{' '}
{getSourceName(source)}
</h3>
<div className={classes.root}>
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
{inLibrary === 'In Library'
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
<div className={classes.root}>
<div className={classes.top}>
<div className={classes.leftRight}>
<div className={classes.leftSide}>
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
</div>
<div className={classes.rightSide}>
<h1>
{manga.title}
</h1>
<h3>
Author:
{' '}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
Artist:
{' '}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>
Status:
{' '}
{manga.status}
</h3>
<h3>
Source:
{' '}
{getSourceName(manga.source)}
</h3>
</div>
</div>
<div className={classes.buttons}>
<div>
<IconButton onClick={() => handleButtonClick()}>
{inLibrary === 'In Library' && <FavoriteIcon />}
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
<span>{inLibrary}</span>
</IconButton>
</div>
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
<a href={manga.url} target="_blank">
<IconButton>
<PublicIcon />
<span>Open Site</span>
</IconButton>
</a>
</div>
</div>
<div className={classes.bottom}>
<div className={classes.description}>
<h4>About</h4>
<p>{manga.description}</p>
</div>
<div className={classes.genre}>
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
</div>
</div>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div>
);
}
+2 -2
View File
@@ -7,7 +7,7 @@ import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard';
interface IProps{
mangas: IManga[]
mangas: IMangaCard[]
message?: string
hasNextPage: boolean
lastPageNum: number
@@ -48,7 +48,7 @@ export default function MangaGrid(props: IProps) {
}
return (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped}
</Grid>
);
+28 -82
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: remove above!
/* 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
@@ -6,18 +5,14 @@
import React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer';
import NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme';
import TemporaryDrawer from './TemporaryDrawer';
const useStyles = makeStyles((theme) => ({
root: {
@@ -31,89 +26,40 @@ const useStyles = makeStyles((theme) => ({
},
}));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() {
const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const { title, action } = useContext(NavBarContext);
const open = Boolean(anchorEl);
const { title, action, override } = useContext(NavBarContext);
const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<div className={classes.root}>
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(true)}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{action}
{/* <IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton> */}
{/* <Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => { setDarkTheme(true); handleClose(); }}
<>
{override.status && override.value}
{!override.status
&& (
<div className={classes.root}>
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(true)}
>
Dark Theme
</MenuItem>
<MenuItem
onClick={() => { setDarkTheme(false); handleClose(); }}
>
Light Theme
</MenuItem>
</Menu> */}
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{action}
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
)}
</>
);
}
+99
View File
@@ -0,0 +1,99 @@
/* eslint-disable react/no-unused-prop-types */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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 CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
import React, { useEffect, useRef, useState } from 'react';
import LazyLoad from 'react-lazyload';
const useStyles = makeStyles({
loading: {
margin: '100px auto',
height: '100vh',
},
loadingImage: {
padding: 'calc(50vh - 40px) calc(50vw - 40px)',
height: '100vh',
width: '100vw',
backgroundColor: '#525252',
marginBottom: 10,
},
});
interface IProps {
src: string
index: number
setCurPage: React.Dispatch<React.SetStateAction<number>>
}
function LazyImage(props: IProps) {
const classes = useStyles();
const { src, index, setCurPage } = props;
const [imageSrc, setImagsrc] = useState<string>('');
const ref = useRef<HTMLImageElement>(null);
const handleScroll = () => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) {
setCurPage(index);
}
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => setImagsrc(src);
}, []);
if (imageSrc.length === 0) {
return (
<div className={classes.loadingImage}>
<CircularProgress thickness={5} />
</div>
);
}
return (
<img
ref={ref}
src={imageSrc}
alt={`Page #${index}`}
style={{ maxWidth: '100%' }}
/>
);
}
export default function Page(props: IProps) {
const { src, index, setCurPage } = props;
const classes = useStyles();
return (
<div style={{ margin: '0 auto' }}>
<LazyLoad
offset={window.innerHeight}
once
placeholder={(
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
)}
>
<LazyImage src={src} index={index} setCurPage={setCurPage} />
</LazyLoad>
</div>
);
}
+208
View File
@@ -0,0 +1,208 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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 IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react';
import Typography from '@material-ui/core/Typography';
import { useHistory } from 'react-router-dom';
import Slide from '@material-ui/core/Slide';
import Fade from '@material-ui/core/Fade';
import Zoom from '@material-ui/core/Zoom';
import { Switch } from '@material-ui/core';
import NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme';
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
// main container and root div need to change classes...
AppMainContainer: {
display: 'none',
},
AppRootElment: {
display: 'flex',
},
root: {
position: settings.staticNav ? 'sticky' : 'fixed',
top: 0,
left: 0,
minWidth: '300px',
height: '100vh',
overflowY: 'auto',
backgroundColor: '#0a0b0b',
'& header': {
backgroundColor: '#363b3d',
display: 'flex',
alignItems: 'center',
minHeight: '64px',
paddingLeft: '24px',
paddingRight: '24px',
transition: 'left 2s ease',
'& button': {
flexGrow: 0,
flexShrink: 0,
},
'& button:nth-child(1)': {
marginRight: '16px',
},
'& button:nth-child(3)': {
marginRight: '-12px',
},
'& h1': {
fontSize: '1.25rem',
flexGrow: 1,
},
},
},
openDrawerButton: {
position: 'fixed',
top: 0 + 20,
left: 10 + 20,
height: '40px',
width: '40px',
borderRadius: 5,
backgroundColor: 'black',
'&:hover': {
backgroundColor: 'black',
},
},
}));
export interface IReaderSettings{
staticNav: boolean
showPageNumber: boolean
}
export const defaultReaderSettings = () => ({
staticNav: false,
showPageNumber: true,
} as IReaderSettings);
interface IProps {
settings: IReaderSettings
setSettings: React.Dispatch<React.SetStateAction<IReaderSettings>>
manga: IMangaCard | IManga
}
export default function ReaderNavBar(props: IProps) {
const { title } = useContext(NavBarContext);
const { darkTheme } = useContext(DarkTheme);
const history = useHistory();
const { settings, setSettings, manga } = props;
const [drawerOpen, setDrawerOpen] = useState(false || settings.staticNav);
const [hideOpenButton, setHideOpenButton] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0);
const theme = useTheme();
const classes = useStyles(settings)();
const setSettingValue = (key: string, value: any) => setSettings({ ...settings, [key]: value });
const handleScroll = () => {
const currentScrollPos = window.pageYOffset;
if (Math.abs(currentScrollPos - prevScrollPos) > 20) {
setHideOpenButton(currentScrollPos > prevScrollPos);
setPrevScrollPos(currentScrollPos);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);// handleScroll changes on every render
useEffect(() => {
window.addEventListener('scroll', handleScroll);
const rootEl = document.querySelector('#root')!;
const mainContainer = document.querySelector('#appMainContainer')!;
rootEl.classList.add(classes.AppRootElment);
mainContainer.classList.add(classes.AppMainContainer);
return () => {
rootEl.classList.remove(classes.AppRootElment);
mainContainer.classList.remove(classes.AppMainContainer);
};
}, [handleScroll]);// handleScroll changes on every render
return (
<>
<Slide direction="right" in={drawerOpen} timeout={200} appear={false}>
<div className={classes.root}>
<header>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
disableRipple
onClick={() => history.push(`/manga/${manga.id}`)}
>
<CloseIcon />
</IconButton>
<Typography variant="h1">
{title}
</Typography>
{!settings.staticNav
&& (
<IconButton
edge="start"
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(false)}
>
<KeyboardArrowLeftIcon />
</IconButton>
) }
</header>
<h3>Static Navigation</h3>
<Switch
checked={settings.staticNav}
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
/>
<h3>Show page number</h3>
<Switch
checked={settings.showPageNumber}
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
/>
</div>
</Slide>
<Zoom in={!drawerOpen}>
<Fade in={!hideOpenButton}>
<IconButton
className={classes.openDrawerButton}
edge="start"
color="inherit"
aria-label="menu"
disableRipple
disableFocusRipple
onClick={() => setDrawerOpen(true)}
>
<KeyboardArrowRightIcon />
</IconButton>
</Fade>
</Zoom>
</>
);
}
+41 -55
View File
@@ -27,68 +27,54 @@ interface IProps {
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
const classes = useStyles();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const sideList = (side: 'left') => (
<div
className={classes.list}
role="presentation"
onClick={() => setDrawerOpen(false)}
onKeyDown={() => setDrawerOpen(false)}
>
<List>
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Extensions" />
</ListItem>
</Link>
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<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>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Global Search" />
</ListItem>
</Link> */}
</List>
</div>
);
return (
<div>
<Drawer
BackdropProps={{ invisible: true }}
open={drawerOpen}
anchor="left"
onClose={() => setDrawerOpen(false)}
>
{sideList('left')}
<div
className={classes.list}
role="presentation"
onClick={() => setDrawerOpen(false)}
onKeyDown={() => setDrawerOpen(false)}
>
<List>
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Extensions" />
</ListItem>
</Link>
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<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>
</List>
</div>
</Drawer>
</div>
);
@@ -9,6 +9,8 @@ type ContextType = {
setTitle: React.Dispatch<React.SetStateAction<string>>
action: any
setAction: React.Dispatch<React.SetStateAction<any>>
override: INavbarOverride
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
};
const NavBarContext = React.createContext<ContextType>({
@@ -16,6 +18,8 @@ const NavBarContext = React.createContext<ContextType>({
setTitle: ():void => {},
action: <div />,
setAction: ():void => {},
override: { status: false, value: <div /> },
setOverride: ():void => {},
});
export default NavBarContext;
+43 -20
View File
@@ -1,22 +1,49 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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, useContext } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { useParams } from 'react-router-dom';
import CircularProgress from '@material-ui/core/CircularProgress';
import ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
const useStyles = makeStyles((theme: Theme) => ({
root: {
[theme.breakpoints.up('md')]: {
display: 'flex',
},
},
chapters: {
listStyle: 'none',
padding: 0,
[theme.breakpoints.up('md')]: {
width: '50%',
marginLeft: '50%',
},
},
loading: {
margin: '10px 0',
display: 'flex',
justifyContent: 'center',
},
}));
export default function Manga() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
const classes = useStyles();
const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
const { id } = useParams<{id: string}>();
const [manga, setManga] = useState<IManga>();
const [source, setSource] = useState<ISource>();
const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => {
@@ -28,32 +55,28 @@ export default function Manga() {
});
}, []);
useEffect(() => {
if (manga !== undefined) {
client.get(`/api/v1/source/${manga.sourceId}`)
.then((response) => response.data)
.then((data: ISource) => {
setSource(data);
});
}
}, [manga]);
useEffect(() => {
client.get(`/api/v1/manga/${id}/chapters`)
.then((response) => response.data)
.then((data) => setChapters(data));
}, []);
const chapterCards = chapters.map((chapter) => (
<ol style={{ listStyle: 'none', padding: 0 }}>
<ChapterCard chapter={chapter} />
const chapterCards = (
<ol className={classes.chapters}>
{chapters.length === 0
&& (
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
) }
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
</ol>
));
);
return (
<>
{(manga && source) && <MangaDetails manga={manga} source={source} />}
<div className={classes.root}>
{manga && <MangaDetails manga={manga} />}
{chapterCards}
</>
</div>
);
}
+75 -21
View File
@@ -1,57 +1,111 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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 CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import Page from '../components/Page';
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
const style = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
backgroundColor: '#343a40',
} as React.CSSProperties;
const useStyles = (settings: IReaderSettings) => makeStyles({
reader: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
},
loading: {
margin: '50px auto',
},
pageNumber: {
display: settings.showPageNumber ? 'block' : 'none',
position: 'fixed',
bottom: '50px',
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
width: '50px',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '10px',
},
});
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
export default function Reader() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Reader'); setAction(<></>); }, []);
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
const classes = useStyles(settings)();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [pageCount, setPageCount] = useState<number>(-1);
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
const [pageCount, setPageCount] = useState<number>(-1);
const [curPage, setCurPage] = useState<number>(0);
const { setOverride, setTitle } = useContext(NavbarContext);
useEffect(() => {
setOverride(
{
status: true,
value: <ReaderNavBar
settings={settings}
setSettings={setSettings}
manga={manga}
/>,
},
);
// clean up for when we leave the reader
return () => setOverride({ status: false, value: <div /> });
}, [manga, settings]);
useEffect(() => {
setTitle('Reader');
client.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, []);
useEffect(() => {
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
.then((response) => response.data)
.then((data:IChapter) => {
setTitle(data.name);
setPageCount(data.pageCount);
});
}, []);
if (pageCount === -1) {
return (
<div style={style}>
<h3>wait</h3>
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
);
}
const mapped = range(pageCount).map((index) => (
<div style={{ margin: '0 auto' }}>
<img src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="F" style={{ maxWidth: '100%' }} />
</div>
));
return (
<div style={style}>
{mapped}
<div className={classes.reader}>
<div className={classes.pageNumber}>
{`${curPage + 1} / ${pageCount}`}
</div>
{range(pageCount).map((index) => (
<Page
key={index}
index={index}
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`}
setCurPage={setCurPage}
/>
))}
</div>
);
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default function SearchSingle() {
const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]);
const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [message, setMessage] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
+1 -1
View File
@@ -13,7 +13,7 @@ export default function SourceMangas(props: { popular: boolean }) {
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>();
const [mangas, setMangas] = useState<IManga[]>([]);
const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
+24 -3
View File
@@ -21,12 +21,28 @@ interface ISource {
history: any
}
interface IManga {
interface IMangaCard {
id: number
sourceId?: string
title: string
thumbnailUrl: string
inLibrary?: boolean
}
interface IManga {
id: number
sourceId: string
url: string
title: string
thumbnailUrl: string
artist: string
author: string
description: string
genre: string
status: string
inLibrary: boolean
source: ISource
}
interface IChapter {
@@ -46,3 +62,8 @@ interface ICategory {
name: String
isLanding: boolean
}
interface INavbarOverride {
status: boolean
value: any
}
+6 -5
View File
@@ -2,19 +2,20 @@
* 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 { useState, Dispatch, SetStateAction } from 'react';
import React, { useState, Dispatch, SetStateAction } from 'react';
import storage from './localStorage';
// eslint-disable-next-line max-len
export default function useLocalStorage<T>(key: string, defaultValue: T) : [T, Dispatch<SetStateAction<T>>] {
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, defaultValue));
export default function useLocalStorage<T>(key: string, defaultValue: T | (() => T)) : [T, Dispatch<SetStateAction<T>>] {
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, initialState));
const setValue = (value: T | ((prevState: T) => T)) => {
const setValue = ((value: T | ((prevState: T) => T)) => {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
storage.setItem(key, valueToStore);
};
}) as React.Dispatch<React.SetStateAction<T>>;
return [storedValue, setValue];
}
+12
View File
@@ -1846,6 +1846,13 @@
dependencies:
"@types/react" "*"
"@types/react-lazyload@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/react-lazyload/-/react-lazyload-3.1.0.tgz#97b167266afbc75f432eca01c50555adae21c5a4"
integrity sha512-JnVJb+6cUrIk4Fo/zc/4NuFtm0h3XeNlN4Gt++WEHGeUDtlhnF1lXRz0WoqNmh5gH3oyeYOJXIZ8MoPL9ehp0g==
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.1.6":
version "5.1.6"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb"
@@ -9353,6 +9360,11 @@ react-is@^16.13.1, 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"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
react-lazyload@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-3.2.0.tgz#497bd06a6dbd7015e3376e1137a67dc47d2dd021"
integrity sha512-zJlrG8QyVZz4+xkYZH5v1w3YaP5wEFaYSUWC4CT9UXfK75IfRAIEdnyIUF+dXr3kX2MOtL1lUaZmaQZqrETwgw==
react-redux@^7.1.1:
version "7.2.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"