diff --git a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt index 1bfab607..40e0c40f 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt @@ -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 -> diff --git a/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/ChapterDataClass.kt b/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/ChapterDataClass.kt index 0a64bf5a..8b59a03a 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/ChapterDataClass.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/ChapterDataClass.kt @@ -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, ) diff --git a/server/src/main/kotlin/ir/armor/tachidesk/database/table/ChapterTable.kt b/server/src/main/kotlin/ir/armor/tachidesk/database/table/ChapterTable.kt index 111cccd6..bc4e8fd1 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/database/table/ChapterTable.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/database/table/ChapterTable.kt @@ -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) } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/Chapter.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/Chapter.kt index b4daf919..4cae7033 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/Chapter.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/Chapter.kt @@ -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 { val mangaDetails = getManga(mangaId) @@ -27,8 +29,10 @@ fun getChapterList(mangaId: Int): List { } ).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 { 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 { 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() ) diff --git a/webUI/react/src/App.tsx b/webUI/react/src/App.tsx index e5be0357..c4ce248e 100644 --- a/webUI/react/src/App.tsx +++ b/webUI/react/src/App.tsx @@ -88,7 +88,7 @@ export default function App() { - + <> @@ -115,7 +115,7 @@ export default function App() { - + diff --git a/webUI/react/src/components/ChapterCard.tsx b/webUI/react/src/components/ChapterCard.tsx index 8a6aa285..0aa65e9d 100644 --- a/webUI/react/src/components/ChapterCard.tsx +++ b/webUI/react/src/components/ChapterCard.tsx @@ -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,7 +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 { useHistory } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; const useStyles = makeStyles((theme) => ({ root: { @@ -65,9 +66,19 @@ export default function ChapterCard(props: IProps) { -
- -
+ + + + diff --git a/webUI/react/src/components/Page.tsx b/webUI/react/src/components/Page.tsx index 2b45f0a1..c738a060 100644 --- a/webUI/react/src/components/Page.tsx +++ b/webUI/react/src/components/Page.tsx @@ -8,16 +8,17 @@ 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 = makeStyles({ +const useStyles = (settings: IReaderSettings) => makeStyles({ loading: { margin: '100px auto', height: '100vh', }, loadingImage: { - padding: 'calc(50vh - 40px) calc(50vw - 40px)', + padding: settings.staticNav ? 'calc(50vh - 40px) calc(50vw - 340px)' : 'calc(50vh - 40px) calc(50vw - 40px)', height: '100vh', - width: '100vw', + width: '200px', backgroundColor: '#525252', marginBottom: 10, }, @@ -27,11 +28,15 @@ interface IProps { src: string index: number setCurPage: React.Dispatch> + settings: IReaderSettings } function LazyImage(props: IProps) { - const classes = useStyles(); - const { src, index, setCurPage } = props; + const { + src, index, setCurPage, settings, + } = props; + + const classes = useStyles(settings)(); const [imageSrc, setImagsrc] = useState(''); const ref = useRef(null); @@ -57,7 +62,7 @@ function LazyImage(props: IProps) { img.src = src; img.onload = () => setImagsrc(src); - }, []); + }, [src]); if (imageSrc.length === 0) { return ( @@ -72,27 +77,33 @@ function LazyImage(props: IProps) { ref={ref} src={imageSrc} alt={`Page #${index}`} - style={{ maxWidth: '100%' }} + style={{ width: '100%' }} /> ); } export default function Page(props: IProps) { - const { src, index, setCurPage } = props; - const classes = useStyles(); + const { + src, index, setCurPage, settings, + } = props; + const classes = useStyles(settings)(); return (
)} > - + ); diff --git a/webUI/react/src/components/ReaderNavBar.tsx b/webUI/react/src/components/ReaderNavBar.tsx index f92f444d..915efcb4 100644 --- a/webUI/react/src/components/ReaderNavBar.tsx +++ b/webUI/react/src/components/ReaderNavBar.tsx @@ -7,16 +7,25 @@ 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 } from 'react-router-dom'; +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 NavBarContext from '../context/NavbarContext'; +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 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... @@ -64,6 +73,49 @@ const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({ 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: { @@ -94,7 +146,9 @@ export const defaultReaderSettings = () => ({ interface IProps { settings: IReaderSettings setSettings: React.Dispatch> - manga: IMangaCard | IManga + manga: IManga | IMangaCard + chapter: IChapter | IPartialChpter + curPage: number } export default function ReaderNavBar(props: IProps) { @@ -103,11 +157,14 @@ export default function ReaderNavBar(props: IProps) { const history = useHistory(); - const { settings, setSettings, manga } = props; + const { + settings, setSettings, manga, chapter, curPage, + } = props; const [drawerOpen, setDrawerOpen] = 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)(); @@ -176,16 +233,92 @@ export default function ReaderNavBar(props: IProps) { ) } -

Static Navigation

- setSettingValue('staticNav', e.target.checked)} - /> -

Show page number

- setSettingValue('showPageNumber', e.target.checked)} - /> + + + + setSettingsCollapseOpen(!settingsCollapseOpen)} + > + {settingsCollapseOpen && } + {!settingsCollapseOpen && } + + + + + + + + + setSettingValue('staticNav', e.target.checked)} + /> + + + + + + setSettingValue('showPageNumber', e.target.checked)} + /> + + + + +
+
+ + Currently on page + {' '} + {curPage + 1} + {' '} + of + {' '} + {chapter.pageCount} + +
+ {chapter.chapterIndex > 1 + && ( + + + + )} + {chapter.chapterIndex < chapter.chapterCount + && ( + + + + )} +
+
diff --git a/webUI/react/src/screens/Reader.tsx b/webUI/react/src/screens/Reader.tsx index 271fbef3..2c0a2d45 100644 --- a/webUI/react/src/screens/Reader.tsx +++ b/webUI/react/src/screens/Reader.tsx @@ -38,6 +38,7 @@ const useStyles = (settings: IReaderSettings) => makeStyles({ }); const range = (n:number) => Array.from({ length: n }, (value, key) => key); +const initialChapter = () => ({ pageCount: -1, chapterIndex: -1, chapterCount: 0 }); export default function Reader() { const [settings, setSettings] = useLocalStorage('readerSettings', defaultReaderSettings); @@ -46,9 +47,9 @@ export default function Reader() { const [serverAddress] = useLocalStorage('serverBaseURL', ''); - const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>(); + const { chapterIndex, mangaId } = useParams<{chapterIndex: string, mangaId: string}>(); const [manga, setManga] = useState({ id: +mangaId, title: '', thumbnailUrl: '' }); - const [pageCount, setPageCount] = useState(-1); + const [chapter, setChapter] = useState(initialChapter()); const [curPage, setCurPage] = useState(0); const { setOverride, setTitle } = useContext(NavbarContext); @@ -56,17 +57,21 @@ export default function Reader() { setOverride( { status: true, - value: , + value: ( + + ), }, ); // clean up for when we leave the reader return () => setOverride({ status: false, value:
}); - }, [manga, settings]); + }, [manga, chapter, settings, curPage, chapterIndex]); useEffect(() => { setTitle('Reader'); @@ -76,17 +81,18 @@ export default function Reader() { setManga(data); setTitle(data.title); }); - }, []); + }, [chapterIndex]); useEffect(() => { - client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`) + setChapter(initialChapter); + client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`) .then((response) => response.data) .then((data:IChapter) => { - setPageCount(data.pageCount); + setChapter(data); }); - }, []); + }, [chapterIndex]); - if (pageCount === -1) { + if (chapter.pageCount === -1) { return (
@@ -96,14 +102,15 @@ export default function Reader() { return (
- {`${curPage + 1} / ${pageCount}`} + {`${curPage + 1} / ${chapter.pageCount}`}
- {range(pageCount).map((index) => ( + {range(chapter.pageCount).map((index) => ( ))}
diff --git a/webUI/react/src/typings.d.ts b/webUI/react/src/typings.d.ts index fab738b3..9de79607 100644 --- a/webUI/react/src/typings.d.ts +++ b/webUI/react/src/typings.d.ts @@ -53,9 +53,17 @@ 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