Compare commits

..

14 Commits

Author SHA1 Message Date
Aria Moradi c6e57e2700 fix typo
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 18s
2021-03-23 05:58:45 +04:30
Aria Moradi c5f467ce3d add no-webUI jar 2021-03-23 05:49:56 +04:30
Aria Moradi 85ec2ed367 drawer hide on click outside of it 2021-03-23 04:28:23 +04:30
Aria Moradi bf908c4d17 chapter prev/next UI+Backend 2021-03-23 03:50:55 +04:30
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
26 changed files with 1027 additions and 253 deletions
+15
View File
@@ -58,6 +58,21 @@ jobs:
**/react/node_modules **/react/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Build no-webUI Jar
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar -x :webUI:copyBuild --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Rename the no-webUI Jar
run: |
cd master/server/build
mv Tachidesk-*.jar $(ls *.jar | sed 's/\.jar/-no-webUI\.jar/g')
- name: Build Jar and launch4j - name: Build Jar and launch4j
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
with: with:
+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. 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 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 ### 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. 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. 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 ### Windows
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`. 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. Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting ## General troubleshooting
+1 -1
View File
@@ -9,7 +9,7 @@ plugins {
id("edu.sc.seis.launch4j") version "2.4.9" id("edu.sc.seis.launch4j") version "2.4.9"
} }
val TachideskVersion = "v0.2.5" val TachideskVersion = "v0.2.6"
repositories { repositories {
@@ -172,10 +172,10 @@ class Main {
ctx.json(getChapterList(mangaId)) ctx.json(getChapterList(mangaId))
} }
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapter(chapterId, mangaId)) ctx.json(getChapter(chapterIndex, mangaId))
} }
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
@@ -12,5 +12,7 @@ data class ChapterDataClass(
val chapter_number: Float, val chapter_number: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, val mangaId: Int,
val chapterIndex: Int,
val chapterCount: Int,
val pageCount: Int? = null, val pageCount: Int? = null,
) )
@@ -13,5 +13,7 @@ object ChapterTable : IntIdTable() {
val chapter_number = float("chapter_number").default(-1f) val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable() val scanlator = varchar("scanlator", 128).nullable()
val chapterIndex = integer("number_in_list")
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
@@ -14,7 +14,9 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
fun getChapterList(mangaId: Int): List<ChapterDataClass> { fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) val mangaDetails = getManga(mangaId)
@@ -27,8 +29,10 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
} }
).toBlocking().first() ).toBlocking().first()
val chapterCount = chapterList.count()
return transaction { return transaction {
chapterList.forEach { fetchedChapter -> chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) { if (chapterEntry == null) {
ChapterTable.insertAndGetId { ChapterTable.insertAndGetId {
@@ -38,12 +42,29 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
it[chapter_number] = fetchedChapter.chapter_number it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
} else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId it[manga] = mangaId
} }
} }
} }
return@transaction chapterList.map { // clear any orphaned chapters
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
if (dbChapterCount > chapterCount) { // we got some clean up due
// TODO
}
return@transaction chapterList.mapIndexed { index, it ->
ChapterDataClass( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url, it.url,
@@ -51,16 +72,19 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId mangaId,
chapterCount - index,
chapterCount
) )
} }
} }
} }
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass { fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
return transaction { return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! val chapterEntry = ChapterTable.select {
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
@@ -71,14 +95,20 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
} }
).toBlocking().first() ).toBlocking().first()
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() }
val chapter = ChapterDataClass( val chapter = ChapterDataClass(
chapterEntry[ChapterTable.id].value, chapterId,
chapterEntry[ChapterTable.url], chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name], chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload], chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number], chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator], chapterEntry[ChapterTable.scanlator],
mangaId, mangaId,
chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(),
pageList.count() pageList.count()
) )
+2
View File
@@ -8,11 +8,13 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/react-lazyload": "^3.1.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"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-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-lazyload": "^3.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.1",
"web-vitals": "^0.2.4" "web-vitals": "^0.2.4"
+17 -4
View File
@@ -27,9 +27,12 @@ import useLocalStorage from './util/useLocalStorage';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />); const [action, setAction] = useState<any>(<div />);
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true); const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = { const navBarContext = {
title, setTitle, action, setAction, title, setTitle, action, setAction, override, setOverride,
}; };
const darkThemeContext = { darkTheme, setDarkTheme }; const darkThemeContext = { darkTheme, setDarkTheme };
@@ -63,7 +66,12 @@ export default function App() {
<NavbarContext.Provider value={navBarContext}> <NavbarContext.Provider value={navBarContext}>
<CssBaseline /> <CssBaseline />
<NavBar /> <NavBar />
<Container maxWidth={false} disableGutters> <Container
id="appMainContainer"
maxWidth={false}
disableGutters
style={{ paddingTop: '64px' }}
>
<Switch> <Switch>
<Route path="/sources/:sourceId/search/"> <Route path="/sources/:sourceId/search/">
<Search /> <Search />
@@ -80,8 +88,8 @@ export default function App() {
<Route path="/sources"> <Route path="/sources">
<Sources /> <Sources />
</Route> </Route>
<Route path="/manga/:mangaId/chapter/:chapterId"> <Route path="/manga/:mangaId/chapter/:chapterNum">
<Reader /> <></>
</Route> </Route>
<Route path="/manga/:id"> <Route path="/manga/:id">
<Manga /> <Manga />
@@ -106,6 +114,11 @@ export default function App() {
/> />
</Switch> </Switch>
</Container> </Container>
<Switch>
<Route path="/manga/:mangaId/chapter/:chapterIndex">
<Reader />
</Route>
</Switch>
</NavbarContext.Provider> </NavbarContext.Provider>
</ThemeProvider> </ThemeProvider>
</Router> </Router>
@@ -85,6 +85,14 @@ export default function CategorySelect(props: IProps) {
<DialogTitle>Set categories</DialogTitle> <DialogTitle>Set categories</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<FormGroup> <FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => ( {categoryInfos.map((categoryInfo) => (
<FormControlLabel <FormControlLabel
control={( control={(
+16 -3
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -8,6 +9,7 @@ import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link, useHistory } from 'react-router-dom';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -41,6 +43,7 @@ interface IProps{
export default function ChapterCard(props: IProps) { export default function ChapterCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
const history = useHistory();
const { chapter } = props; const { chapter } = props;
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10); const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
@@ -63,9 +66,19 @@ export default function ChapterCard(props: IProps) {
</Typography> </Typography>
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}> <Link
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button> to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
</div> style={{ textDecoration: 'none' }}
>
<Button
variant="outlined"
style={{ marginLeft: 20 }}
>
open
</Button>
</Link>
</CardContent> </CardContent>
</Card> </Card>
</li> </li>
+1 -1
View File
@@ -43,7 +43,7 @@ const useStyles = makeStyles({
}); });
interface IProps { interface IProps {
manga: IManga manga: IMangaCard
} }
const MangaCard = React.forwardRef((props: IProps, ref) => { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {
+196 -33
View File
@@ -2,55 +2,180 @@
* 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 { Button, createStyles, makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react'; 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 client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import CategorySelect from './CategorySelect'; import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({ const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: { root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'fixed',
width: '50vw',
},
},
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
leftRight: {
display: 'flex', 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': { '& 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{ interface IProps{
manga: IManga manga: IManga
source: ISource
} }
function getSourceName(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; return source.id;
} }
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) { export default function MangaDetails(props: IProps) {
const classes = useStyles(); const { setAction } = useContext(NavbarContext);
const { manga, source } = props;
const { manga } = props;
const [inLibrary, setInLibrary] = useState<string>( 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); 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() { function addToLibrary() {
setInLibrary('adding'); // setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => { client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library'); setInLibrary('In Library');
}); });
} }
function removeFromLibrary() { function removeFromLibrary() {
setInLibrary('removing'); // setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => { client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Not In Library'); setInLibrary('Add To Library');
}); });
} }
function handleButtonClick() { function handleButtonClick() {
if (inLibrary === 'Not In Library') { if (inLibrary === 'Add To Library') {
addToLibrary(); addToLibrary();
} else { } else {
removeFromLibrary(); removeFromLibrary();
@@ -58,26 +183,64 @@ export default function MangaDetails(props: IProps) {
} }
return ( return (
<div> <div className={classes.root}>
<h1> <div className={classes.top}>
{manga.title} <div className={classes.leftRight}>
</h1> <div className={classes.leftSide}>
<h3> <img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
Source: </div>
{' '} <div className={classes.rightSide}>
{getSourceName(source)} <h1>
</h3> {manga.title}
<div className={classes.root}> </h1>
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button> <h3>
{inLibrary === 'In Library' Author:
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>} {' '}
<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> </div>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div> </div>
); );
} }
+2 -2
View File
@@ -7,7 +7,7 @@ import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard'; import MangaCard from './MangaCard';
interface IProps{ interface IProps{
mangas: IManga[] mangas: IMangaCard[]
message?: string message?: string
hasNextPage: boolean hasNextPage: boolean
lastPageNum: number lastPageNum: number
@@ -48,7 +48,7 @@ export default function MangaGrid(props: IProps) {
} }
return ( return (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}> <Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped} {mapped}
</Grid> </Grid>
); );
+28 -82
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: remove above! // TODO: remove above!
/* 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
@@ -6,18 +5,14 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; 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 NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme'; import DarkTheme from '../context/DarkTheme';
import TemporaryDrawer from './TemporaryDrawer';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -31,89 +26,40 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() { export default function NavBar() {
const classes = useStyles(); const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const { title, action, override } = useContext(NavBarContext);
const { title, action } = useContext(NavBarContext);
const open = Boolean(anchorEl);
const { darkTheme } = useContext(DarkTheme); const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return ( return (
<div className={classes.root}> <>
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}> {override.status && override.value}
<Toolbar> {!override.status
<IconButton && (
edge="start" <div className={classes.root}>
className={classes.menuButton} <AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
color="inherit" <Toolbar>
aria-label="menu" <IconButton
disableRipple edge="start"
onClick={() => setDrawerOpen(true)} className={classes.menuButton}
> color="inherit"
<MenuIcon /> aria-label="menu"
</IconButton> disableRipple
<Typography variant="h6" className={classes.title}> onClick={() => setDrawerOpen(true)}
{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(); }}
> >
Dark Theme <MenuIcon />
</IconButton>
</MenuItem> <Typography variant="h6" className={classes.title}>
<MenuItem {title}
onClick={() => { setDarkTheme(false); handleClose(); }} </Typography>
> {action}
Light Theme </Toolbar>
</AppBar>
</MenuItem> <TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</Menu> */} </div>
</Toolbar> )}
</AppBar> </>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
); );
} }
+110
View File
@@ -0,0 +1,110 @@
/* 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';
import { IReaderSettings } from './ReaderNavBar';
const useStyles = (settings: IReaderSettings) => makeStyles({
loading: {
margin: '100px auto',
height: '100vh',
},
loadingImage: {
padding: settings.staticNav ? 'calc(50vh - 40px) calc(50vw - 340px)' : 'calc(50vh - 40px) calc(50vw - 40px)',
height: '100vh',
width: '200px',
backgroundColor: '#525252',
marginBottom: 10,
},
});
interface IProps {
src: string
index: number
setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings
}
function LazyImage(props: IProps) {
const {
src, index, setCurPage, settings,
} = props;
const classes = useStyles(settings)();
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);
}, [src]);
if (imageSrc.length === 0) {
return (
<div className={classes.loadingImage}>
<CircularProgress thickness={5} />
</div>
);
}
return (
<img
ref={ref}
src={imageSrc}
alt={`Page #${index}`}
style={{ width: '100%' }}
/>
);
}
export default function Page(props: IProps) {
const {
src, index, setCurPage, settings,
} = props;
const classes = useStyles(settings)();
return (
<div style={{ margin: '0 auto' }}>
<LazyLoad
offset={window.innerHeight}
placeholder={(
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
)}
>
<LazyImage
src={src}
index={index}
setCurPage={setCurPage}
settings={settings}
/>
</LazyLoad>
</div>
);
}
+347
View File
@@ -0,0 +1,347 @@
/* 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 KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
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, Link } 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 List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Collapse from '@material-ui/core/Collapse';
import Button from '@material-ui/core/Button';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import DarkTheme from '../context/DarkTheme';
import NavBarContext from '../context/NavbarContext';
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,
},
},
'& hr': {
margin: '0 16px',
height: '1px',
border: '0',
backgroundColor: 'rgb(38, 41, 43)',
},
},
navigation: {
margin: '0 16px',
'& > span:nth-child(1)': {
textAlign: 'center',
display: 'block',
marginTop: '16px',
},
'& $navigationChapters': {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridTemplateAreas: '"prev next"',
gridColumnGap: '5px',
margin: '10px 0',
'& a': {
flexGrow: 1,
textDecoration: 'none',
'& button': {
width: '100%',
padding: '5px 8px',
textTransform: 'none',
},
},
},
},
navigationChapters: {}, // dummy rule
settingsCollapsseHeader: {
'& span': {
fontWeight: 'bold',
},
},
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: IManga | IMangaCard
chapter: IChapter | IPartialChpter
curPage: number
}
export default function ReaderNavBar(props: IProps) {
const { title } = useContext(NavBarContext);
const { darkTheme } = useContext(DarkTheme);
const history = useHistory();
const {
settings, setSettings, manga, chapter, curPage,
} = props;
const [drawerOpen, setDrawerOpen] = useState(false || settings.staticNav);
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
const [hideOpenButton, setHideOpenButton] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(false);
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);
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);
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);// handleScroll changes on every render
return (
<>
<ClickAwayListener onClickAway={() => (drawerVisible && setDrawerOpen(false))}>
<Slide
direction="right"
in={drawerOpen}
timeout={200}
appear={false}
mountOnEnter
unmountOnExit
onEntered={() => setDrawerVisible(true)}
onExited={() => setDrawerVisible(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>
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
<ListItemText primary="Reader Settings" />
<ListItemSecondaryAction>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
disableRipple
disableFocusRipple
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
>
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
<List>
<ListItem>
<ListItemText primary="Static Navigation" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.staticNav}
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Show page number" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.showPageNumber}
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Collapse>
<hr />
<div className={classes.navigation}>
<span>
Currently on page
{' '}
{curPage + 1}
{' '}
of
{' '}
{chapter.pageCount}
</span>
<div className={classes.navigationChapters}>
{chapter.chapterIndex > 1
&& (
<Link
style={{ gridArea: 'prev' }}
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex - 1}`}
>
<Button
variant="outlined"
startIcon={<KeyboardArrowLeftIcon />}
>
Chapter
{' '}
{chapter.chapterIndex - 1}
</Button>
</Link>
)}
{chapter.chapterIndex < chapter.chapterCount
&& (
<Link
style={{ gridArea: 'next' }}
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex + 1}`}
>
<Button
variant="outlined"
endIcon={<KeyboardArrowRightIcon />}
>
Chapter
{' '}
{chapter.chapterIndex + 1}
</Button>
</Link>
)}
</div>
</div>
</div>
</Slide>
</ClickAwayListener>
<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) { export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
const classes = useStyles(); 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 ( return (
<div> <div>
<Drawer <Drawer
BackdropProps={{ invisible: true }}
open={drawerOpen} open={drawerOpen}
anchor="left" anchor="left"
onClose={() => setDrawerOpen(false)} 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> </Drawer>
</div> </div>
); );
@@ -9,6 +9,8 @@ type ContextType = {
setTitle: React.Dispatch<React.SetStateAction<string>> setTitle: React.Dispatch<React.SetStateAction<string>>
action: any action: any
setAction: React.Dispatch<React.SetStateAction<any>> setAction: React.Dispatch<React.SetStateAction<any>>
override: INavbarOverride
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
}; };
const NavBarContext = React.createContext<ContextType>({ const NavBarContext = React.createContext<ContextType>({
@@ -16,6 +18,8 @@ const NavBarContext = React.createContext<ContextType>({
setTitle: ():void => {}, setTitle: ():void => {},
action: <div />, action: <div />,
setAction: ():void => {}, setAction: ():void => {},
override: { status: false, value: <div /> },
setOverride: ():void => {},
}); });
export default NavBarContext; 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 /* 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
* 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 React, { useEffect, useState, useContext } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import CircularProgress from '@material-ui/core/CircularProgress';
import ChapterCard from '../components/ChapterCard'; import ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails'; import MangaDetails from '../components/MangaDetails';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from '../context/NavbarContext';
import client from '../util/client'; 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() { export default function Manga() {
const { setTitle, setAction } = useContext(NavbarContext); const classes = useStyles();
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
const { id } = useParams<{id: string}>(); const { id } = useParams<{id: string}>();
const [manga, setManga] = useState<IManga>(); const [manga, setManga] = useState<IManga>();
const [source, setSource] = useState<ISource>();
const [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => { 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(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/chapters`) client.get(`/api/v1/manga/${id}/chapters`)
.then((response) => response.data) .then((response) => response.data)
.then((data) => setChapters(data)); .then((data) => setChapters(data));
}, []); }, []);
const chapterCards = chapters.map((chapter) => ( const chapterCards = (
<ol style={{ listStyle: 'none', padding: 0 }}> <ol className={classes.chapters}>
<ChapterCard chapter={chapter} /> {chapters.length === 0
&& (
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
) }
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
</ol> </ol>
)); );
return ( return (
<> <div className={classes.root}>
{(manga && source) && <MangaDetails manga={manga} source={source} />} {manga && <MangaDetails manga={manga} />}
{chapterCards} {chapterCards}
</> </div>
); );
} }
+87 -26
View File
@@ -1,57 +1,118 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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
* 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 CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Page from '../components/Page';
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from '../context/NavbarContext';
import client from '../util/client'; import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from '../util/useLocalStorage';
const style = { const useStyles = (settings: IReaderSettings) => makeStyles({
display: 'flex', reader: {
flexDirection: 'column', display: 'flex',
justifyContent: 'center', flexDirection: 'column',
margin: '0 auto', justifyContent: 'center',
backgroundColor: '#343a40', margin: '0 auto',
} as React.CSSProperties; },
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); const range = (n:number) => Array.from({ length: n }, (value, key) => key);
const initialChapter = () => ({ pageCount: -1, chapterIndex: -1, chapterCount: 0 });
export default function Reader() { export default function Reader() {
const { setTitle, setAction } = useContext(NavbarContext); const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
useEffect(() => { setTitle('Reader'); setAction(<></>); }, []);
const classes = useStyles(settings)();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [pageCount, setPageCount] = useState<number>(-1); const { chapterIndex, mangaId } = useParams<{chapterIndex: string, mangaId: string}>();
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>(); const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
const [curPage, setCurPage] = useState<number>(0);
const { setOverride, setTitle } = useContext(NavbarContext);
useEffect(() => {
setOverride(
{
status: true,
value: (
<ReaderNavBar
settings={settings}
setSettings={setSettings}
manga={manga}
chapter={chapter}
curPage={curPage}
/>
),
},
);
// clean up for when we leave the reader
return () => setOverride({ status: false, value: <div /> });
}, [manga, chapter, settings, curPage, chapterIndex]);
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`) setTitle('Reader');
client.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, [chapterIndex]);
useEffect(() => {
setChapter(initialChapter);
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
.then((response) => response.data) .then((response) => response.data)
.then((data:IChapter) => { .then((data:IChapter) => {
setTitle(data.name); setChapter(data);
setPageCount(data.pageCount);
}); });
}, []); }, [chapterIndex]);
if (pageCount === -1) { if (chapter.pageCount === -1) {
return ( return (
<div style={style}> <div className={classes.loading}>
<h3>wait</h3> <CircularProgress thickness={5} />
</div> </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 ( return (
<div style={style}> <div className={classes.reader}>
{mapped} <div className={classes.pageNumber}>
{`${curPage + 1} / ${chapter.pageCount}`}
</div>
{range(chapter.pageCount).map((index) => (
<Page
key={index}
index={index}
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`}
setCurPage={setCurPage}
settings={settings}
/>
))}
</div> </div>
); );
} }
+1 -1
View File
@@ -27,7 +27,7 @@ export default function SearchSingle() {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles(); const classes = useStyles();
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false); 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(<></>); }, []); useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false); const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1); const [lastPageNum, setLastPageNum] = useState<number>(1);
+32 -3
View File
@@ -21,12 +21,28 @@ interface ISource {
history: any history: any
} }
interface IManga { interface IMangaCard {
id: number id: number
sourceId?: string
title: string title: string
thumbnailUrl: 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 { interface IChapter {
@@ -37,12 +53,25 @@ interface IChapter {
chapter_number: number chapter_number: number
scanlator: String scanlator: String
mangaId: number mangaId: number
chapterIndex: number
chapterCount: number
pageCount: number pageCount: number
} }
interface IPartialChpter {
pageCount: number
chapterIndex: number
chapterCount: number
}
interface ICategory { interface ICategory {
id: number id: number
order: number order: number
name: String name: String
isLanding: boolean 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 * 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 { useState, Dispatch, SetStateAction } from 'react'; import React, { useState, Dispatch, SetStateAction } from 'react';
import storage from './localStorage'; import storage from './localStorage';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export default function useLocalStorage<T>(key: string, defaultValue: T) : [T, Dispatch<SetStateAction<T>>] { export default function useLocalStorage<T>(key: string, defaultValue: T | (() => T)) : [T, Dispatch<SetStateAction<T>>] {
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, defaultValue)); 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 // Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value; const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore); setStoredValue(valueToStore);
storage.setItem(key, valueToStore); storage.setItem(key, valueToStore);
}; }) as React.Dispatch<React.SetStateAction<T>>;
return [storedValue, setValue]; return [storedValue, setValue];
} }
+12
View File
@@ -1846,6 +1846,13 @@
dependencies: dependencies:
"@types/react" "*" "@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": "@types/react-router-dom@^5.1.6":
version "5.1.6" version "5.1.6"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb" 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" 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-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: react-redux@^7.1.1:
version "7.2.2" version "7.2.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"