Compare commits
14 Commits
v0.2.5
...
v0.2.6-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
| c6e57e2700 | |||
| c5f467ce3d | |||
| 85ec2ed367 | |||
| bf908c4d17 | |||
| f41c5c9428 | |||
| 04837983fa | |||
| 5d484b012c | |||
| 436a8d0585 | |||
| 28cc0a6f84 | |||
| 26cc2f2c96 | |||
| 149107e749 | |||
| a74936c5f5 | |||
| ff8c8913d4 | |||
| 83426e1302 |
@@ -58,6 +58,21 @@ jobs:
|
||||
**/react/node_modules
|
||||
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
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -172,10 +172,10 @@ class Main {
|
||||
ctx.json(getChapterList(mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").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 ->
|
||||
|
||||
@@ -12,5 +12,7 @@ data class ChapterDataClass(
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
val chapterIndex: Int,
|
||||
val chapterCount: Int,
|
||||
val pageCount: Int? = null,
|
||||
)
|
||||
|
||||
@@ -13,5 +13,7 @@ object ChapterTable : IntIdTable() {
|
||||
val chapter_number = float("chapter_number").default(-1f)
|
||||
val scanlator = varchar("scanlator", 128).nullable()
|
||||
|
||||
val chapterIndex = integer("number_in_list")
|
||||
|
||||
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.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
val mangaDetails = getManga(mangaId)
|
||||
@@ -27,8 +29,10 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
val chapterCount = chapterList.count()
|
||||
|
||||
return transaction {
|
||||
chapterList.forEach { fetchedChapter ->
|
||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||
if (chapterEntry == null) {
|
||||
ChapterTable.insertAndGetId {
|
||||
@@ -38,12 +42,29 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
it[chapter_number] = fetchedChapter.chapter_number
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
||||
it.url,
|
||||
@@ -51,16 +72,19 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
it.date_upload,
|
||||
it.chapter_number,
|
||||
it.scanlator,
|
||||
mangaId
|
||||
mangaId,
|
||||
chapterCount - index,
|
||||
chapterCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||
return transaction {
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||
val chapterEntry = ChapterTable.select {
|
||||
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
|
||||
}.firstOrNull()!!
|
||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
@@ -71,14 +95,20 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
||||
|
||||
val chapter = ChapterDataClass(
|
||||
chapterEntry[ChapterTable.id].value,
|
||||
chapterId,
|
||||
chapterEntry[ChapterTable.url],
|
||||
chapterEntry[ChapterTable.name],
|
||||
chapterEntry[ChapterTable.date_upload],
|
||||
chapterEntry[ChapterTable.chapter_number],
|
||||
chapterEntry[ChapterTable.scanlator],
|
||||
mangaId,
|
||||
chapterEntry[ChapterTable.chapterIndex],
|
||||
chapterCount.toInt(),
|
||||
|
||||
pageList.count()
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+17
-4
@@ -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 />
|
||||
@@ -80,8 +88,8 @@ export default function App() {
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||
<Reader />
|
||||
<Route path="/manga/:mangaId/chapter/:chapterNum">
|
||||
<></>
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
@@ -106,6 +114,11 @@ export default function App() {
|
||||
/>
|
||||
</Switch>
|
||||
</Container>
|
||||
<Switch>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterIndex">
|
||||
<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={(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* 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/. */
|
||||
@@ -8,6 +9,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 { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -41,6 +43,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);
|
||||
@@ -63,9 +66,19 @@ export default function ChapterCard(props: IProps) {
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button>
|
||||
</div>
|
||||
<Link
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
open
|
||||
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
|
||||
@@ -43,7 +43,7 @@ const useStyles = makeStyles({
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
manga: IManga
|
||||
manga: IMangaCard
|
||||
}
|
||||
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||
const {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,118 @@
|
||||
/* 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);
|
||||
const initialChapter = () => ({ pageCount: -1, chapterIndex: -1, chapterCount: 0 });
|
||||
|
||||
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 { chapterIndex, mangaId } = useParams<{chapterIndex: 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(() => {
|
||||
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((data:IChapter) => {
|
||||
setTitle(data.name);
|
||||
setPageCount(data.pageCount);
|
||||
setChapter(data);
|
||||
});
|
||||
}, []);
|
||||
}, [chapterIndex]);
|
||||
|
||||
if (pageCount === -1) {
|
||||
if (chapter.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} / ${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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Vendored
+32
-3
@@ -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 {
|
||||
@@ -37,12 +53,25 @@ interface IChapter {
|
||||
chapter_number: number
|
||||
scanlator: String
|
||||
mangaId: number
|
||||
chapterIndex: number
|
||||
chapterCount: number
|
||||
pageCount: number
|
||||
}
|
||||
|
||||
interface IPartialChpter {
|
||||
pageCount: number
|
||||
chapterIndex: number
|
||||
chapterCount: number
|
||||
}
|
||||
|
||||
interface ICategory {
|
||||
id: number
|
||||
order: number
|
||||
name: String
|
||||
isLanding: boolean
|
||||
}
|
||||
|
||||
interface INavbarOverride {
|
||||
status: boolean
|
||||
value: any
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user