Compare commits

...

52 Commits

Author SHA1 Message Date
Aria Moradi bab25f9ad9 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 23:24:25 +04:30
Aria Moradi a62ee8f8c3 handle reader types 2021-05-15 23:22:37 +04:30
Aria Moradi 5f23691e20 add toast lib 2021-05-15 20:37:07 +04:30
Aria Moradi 3de9ccc62f stop spinning if chapter list is empty 2021-05-15 18:51:38 +04:30
Aria Moradi 1896f7f37b remove lazy load 2021-05-15 18:26:40 +04:30
Aria Moradi 490643dc02 proof of concept readers 2021-05-15 18:17:12 +04:30
Aria Moradi 9808976088 restructure the reader 2021-05-15 17:18:57 +04:30
Aria Moradi 5a73068a10 bump to v0.3.1
CI Publish / Validate Gradle Wrapper (push) Successful in 17s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 14:53:44 +04:30
Aria Moradi 01d5c2540d chapter updates when pressing UI buttons 2021-05-15 13:43:26 +04:30
Aria Moradi 866b01f865 support bookmarked and isRead in webUI 2021-05-14 19:22:10 +04:30
Aria Moradi da6a953099 exposed error 2021-05-14 17:31:07 +04:30
Aria Moradi bce8d58845 make it look nicer? 2021-05-14 02:01:28 +04:30
Aria Moradi 3cfce2db04 fix chapters not shown on movbile 2021-05-13 21:40:42 +04:30
Aria Moradi 327aae5dd9 Merge pull request #79 from Suwayomi/read-category
Support more chapter parameters
2021-05-13 18:34:27 +04:30
Aria Moradi 1bdfde7032 no longer TODO 2021-05-13 18:33:24 +04:30
Aria Moradi 295a0817b0 fix wrong chapters being removed, fix da css 2021-05-13 18:29:20 +04:30
Aria Moradi a02dc02d52 remove console prints 2021-05-13 17:49:33 +04:30
Aria Moradi dc012edf7d staisfying results? with chapters scrolling 2021-05-13 17:46:40 +04:30
Aria Moradi 1e2eb11c13 update dependencies 2021-05-13 15:01:37 +04:30
Aria Moradi 3a825f4f25 fix manga thumbnail loading bug 2021-05-12 10:39:21 +04:30
Aria Moradi b9ea8c5f74 code cleanup 2021-05-12 10:20:01 +04:30
Aria Moradi 320d7e2536 reformat 2021-05-11 18:55:09 +04:30
Aria Moradi c200785479 handle front, handle orphans 2021-05-11 18:45:53 +04:30
Aria Moradi 8abb132ad6 chapter new parameters get endpoint 2021-05-11 15:50:18 +04:30
Aria Moradi 8bb2269f36 [SKIP CI] fix typo 2021-05-08 20:33:53 +04:30
Aria Moradi 9d17b26283 [SKIP CI] up for debate 2021-05-07 19:41:34 +04:30
Aria Moradi 5909f15db7 [SKIP CI] code of conduct 2021-05-07 19:39:24 +04:30
Aria Moradi 11672ca576 [SKIP CI] 2021-05-07 19:35:11 +04:30
Aria Moradi e09773def3 [SKIP CI] potential contributors 2021-05-07 19:32:32 +04:30
Aria Moradi f6d4432e6f [SKIP CI] typo 2021-05-07 19:13:48 +04:30
Aria Moradi 45a6abc5c2 [SKIP CI] improve structure 2021-05-07 19:13:04 +04:30
Aria Moradi dc5e677a38 [SKIP CI] simplify 2021-05-07 19:05:11 +04:30
Aria Moradi a82549dc17 [SKIP CI] why a web app? 2021-05-07 19:02:56 +04:30
Aria Moradi a002e19d9d [SKIP CI] move info to CONTRIBUTING.md 2021-05-07 18:58:09 +04:30
Aria Moradi cdf1f98d28 [SKIP CI] move troubleshooting to wiki 2021-05-07 18:51:47 +04:30
Aria Moradi 0ff1ebdeb7 [SKIP CI] not alternative 2021-05-07 18:36:32 +04:30
Aria Moradi 17f4a396f8 [SKIP CI] one must know friend from foe! 2021-05-07 18:29:01 +04:30
Aria Moradi 8aa3cf4368 [SKIP CI] move stuff around 2021-05-07 18:27:10 +04:30
Aria Moradi 0136c5e493 [SKIP CI] alternative ui projects 2021-05-07 18:21:32 +04:30
Aria Moradi 8b94b9ee80 [SKIP CI] add contributing.md 2021-05-07 18:17:55 +04:30
Aria Moradi bed63f19f2 [SKIP CI] move technical info to contributing.md 2021-05-07 18:04:51 +04:30
Aria Moradi e2a6545a84 [SKIP CI] 2021-05-07 15:28:30 +04:30
Aria Moradi e3d3ec6895 fix sed output 2021-05-07 14:41:19 +04:30
Aria Moradi 7ba476bd79 include v 2021-05-07 14:39:15 +04:30
Aria Moradi 2dd41ebd27 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-07 14:25:11 +04:30
Aria Moradi 038df78171 new preview version format 2021-05-07 14:24:28 +04:30
Aria Moradi 6e5ff2b508 [SKIP CI] it be "chrome OS" 2021-05-07 13:45:31 +04:30
Aria Moradi ec8d1e8680 [SKIP CI] more housekeeping! 2021-05-07 13:42:02 +04:30
Aria Moradi 1f0f0c33b7 [SKIP CI] housekeeping 2021-05-07 13:32:42 +04:30
Aria Moradi 825940fcac [SKIP CI] add running instructions 2021-05-07 13:17:15 +04:30
Aria Moradi 4618834af2 [SKIP CI] Make it simpler 2021-05-07 11:24:40 +04:30
Aria Moradi 55d968df5e [SKIP CI] add some doc comments 2021-05-06 23:06:35 +04:30
35 changed files with 2741 additions and 2138 deletions
@@ -11,7 +11,7 @@ cp -f $new_jar_build Tachidesk-latest.jar
rm -rf latest_pointer/* rm -rf latest_pointer/*
cp $new_jar_build latest_pointer cp $new_jar_build latest_pointer
latest=$(ls *.jar | tail -n1 | cut -d"-" -f3 | cut -d"." -f1) latest=$(ls *.jar | tail -n1 | sed -e's/Tachidesk-\|.jar//g')
echo "{ \"latest\": \"$latest\" }" > index.json echo "{ \"latest\": \"$latest\" }" > index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.email "github-actions[bot]@users.noreply.github.com"
+1 -1
View File
@@ -77,4 +77,4 @@ jobs:
- name: Deploy preview - name: Deploy preview
run: | run: |
./master/.github/scripts/commit-repo.sh ./master/.github/scripts/commit-preview.sh
+5
View File
@@ -0,0 +1,5 @@
# Code Of Conduct
- Don't be a dick.
# expanding the code of conduct!
The contents of this document is up for debate and improvement! Discussions on discord.
+52
View File
@@ -0,0 +1,52 @@
# Contributing
## Where should I start?
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
## How does Tachidesk work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
## Why a web app?
This structure is chosen to
- Achieve the maximum multi-platform-ness
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
- Eaise development of alternative user intefaces for Tachidesk
## User Interfaces for Tachidesk server
Currently there are three known interfaces for Tachidesk:
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
## Building from source
### Prerequisites
You need these software packages installed in order to build the project
### Server
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Android stubs jar
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### webUI
- Nodejs LTS or latest
- Yarn
- Git
### building the full-blown jar
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled(server only)
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
## Running in development mode
First satistify [the prerequisites](#prerequisites)
### server
run `./gradlew :server:run --stacktrace` to run the server
### webUI
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the development server, if a new browser window doesn't get opned automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
+13 -49
View File
@@ -8,9 +8,9 @@
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/). A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is an independent Tachiyomi compatible software made by [@AriaMoradi AKA ArMor](https://github.com/AriaMoradi) and contributors and is **not a Fork of** Tachiyomi. Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
@@ -26,11 +26,9 @@ Here is a list of current features:
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens. **Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/Tachidesk/projects/1) to see what's implemented in more detail.
## Downloading and Running the app ## Downloading and Running the app
### 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(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview). Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
@@ -46,56 +44,22 @@ You can install Tachidesk from the AUR
``` ```
yay -S tachidesk yay -S tachidesk
``` ```
Or the latest preview version
```
yay -S tachidesk-preview
```
### Docker ### 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 ### Using Tachidesk Remotely
If the app breaks, make sure that it's not running(right click on tray icon and quit or kill it through the way your Operating System provides), delete the directory below and re-run the app (**This procedure will delete all your data!**) and if the problem persists open an issue or ask for help on discord. You can run Tachidesk on your computer or a server and connect to it remotely through the web interface with a web browser on any device including a mobile or tablet or even your smart TV!, this method of using Tachidesk is only recommended if you are a power user and know what you are doing.
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk` ## Troubleshooting and Support
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk` ## Contributing and Technical info
See [CONTRIBUTING.md](./CONTRIBUTING.md).
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
## Support and help
Join Tachidesk's [discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Building from source
### Prerequisite: Get Android stubs jar
#### Manual download
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Automated download
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### Prerequisite: Software dependencies
You need this software packages installed in order to build this project:
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Nodejs LTS or latest
- Yarn
- Git
### building the full-blown jar
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled(server only)
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
## Running for development purposes
### `server` module
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run --stacktrace` to run the server
### `webUI` module
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the development server, if a new browser window doesn't get opned automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
## Credit ## Credit
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb. This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
+1 -1
View File
@@ -97,7 +97,7 @@ sourceSets {
} }
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = "v0.3.0" val tachideskVersion = "v0.3.2"
// counts commit count on master // counts commit count on master
val tachideskRevision = Runtime val tachideskRevision = Runtime
@@ -15,8 +15,11 @@ import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.ChapterTable import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable import ir.armor.tachidesk.model.database.table.PageTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.ChapterDataClass import ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@@ -25,53 +28,81 @@ import org.jetbrains.exposed.sql.update
object Chapter { object Chapter {
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> { suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) return if (!onlineFetch) {
val source = getHttpSource(mangaDetails.sourceId.toLong()) transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
val chapterList = source.fetchChapterList( .map {
SManga.create().apply { ChapterTable.toDataClass(it)
title = mangaDetails.title
url = mangaDetails.url
}
).awaitSingle()
val chapterCount = chapterList.count()
return transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insert {
it[url] = 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
} }
} else { }
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { } else {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1 val mangaDetails = getManga(mangaId)
it[manga] = mangaId val source = getHttpSource(mangaDetails.sourceId.toLong())
val chapterList = source.fetchChapterList(
SManga.create().apply {
title = mangaDetails.title
url = mangaDetails.url
}
).awaitSingle()
val chapterCount = chapterList.count()
transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insert {
it[url] = 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
}
} 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
}
} }
} }
} }
// clear any orphaned chapters that are in the db but not in `chapterList` // clear any orphaned chapters that are in the db but not in `chapterList`
val dbChapterCount = transaction { ChapterTable.selectAll().count() } val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
if (dbChapterCount > chapterCount) { // we got some clean up due if (dbChapterCount > chapterCount) { // we got some clean up due
// TODO: delete orphan chapters val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
dbChapterList.forEach {
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
) {
transaction {
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
}
}
}
} }
chapterList.mapIndexed { index, it -> val dbChapterMap = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it })
}
return chapterList.mapIndexed { index, it ->
val dbChapter = dbChapterMap.getValue(it.url)
ChapterDataClass( ChapterDataClass(
it.url, it.url,
it.name, it.name,
@@ -79,6 +110,11 @@ object Chapter {
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId, mangaId,
dbChapter[ChapterTable.isRead],
dbChapter[ChapterTable.isBookmarked],
dbChapter[ChapterTable.lastPageRead],
chapterCount - index, chapterCount - index,
) )
} }
@@ -132,9 +168,37 @@ object Chapter {
chapterEntry[ChapterTable.chapter_number], chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator], chapterEntry[ChapterTable.scanlator],
mangaId, mangaId,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.chapterIndex], chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(), chapterCount.toInt(),
pageList.count() pageList.count()
) )
} }
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update ->
isRead?.also {
update[ChapterTable.isRead] = it
}
isBookmarked?.also {
update[ChapterTable.isBookmarked] = it
}
lastPageRead?.also {
update[ChapterTable.lastPageRead] = it
}
}
}
markPrevRead?.let {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) {
it[ChapterTable.isRead] = markPrevRead
}
}
}
}
} }
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
import ir.armor.tachidesk.impl.Source.getSource import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.await import ir.armor.tachidesk.impl.util.await
@@ -35,17 +36,17 @@ object Manga {
text text
} }
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass { suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return if (mangaEntry[MangaTable.initialized]) { return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
MangaDataClass( MangaDataClass(
mangaId, mangaId,
mangaEntry[MangaTable.sourceReference].toString(), mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url], proxyThumbnailUrl(mangaId),
true, true,
@@ -55,7 +56,8 @@ object Manga {
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference]) getSource(mangaEntry[MangaTable.sourceReference]),
false
) )
} else { // initialize manga } else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
@@ -81,8 +83,9 @@ object Manga {
} }
} }
clearMangaThumbnail(mangaId)
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
MangaDataClass( MangaDataClass(
mangaId, mangaId,
@@ -90,7 +93,7 @@ object Manga {
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail, proxyThumbnailUrl(mangaId),
true, true,
@@ -100,28 +103,37 @@ object Manga {
fetchedManga.genre, fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name, MangaStatus.valueOf(fetchedManga.status).name,
false, false,
getSource(mangaEntry[MangaTable.sourceReference]) getSource(mangaEntry[MangaTable.sourceReference]),
true
) )
} }
} }
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> { suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = applicationDirs.thumbnailsRoot val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
return getCachedImageResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
getManga(mangaId) // make sure is initialized
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference] val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) { val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
source.client.newCall( source.client.newCall(
GET(thumbnailUrl, source.headers) GET(thumbnailUrl, source.headers)
).await() ).await()
} }
} }
suspend fun clearMangaThumbnail(mangaId: Int) {
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
clearCachedImage(saveDir, fileName)
}
} }
@@ -80,11 +80,11 @@ object LegacyBackupImport : LegacyBackupBase() {
return validationResult return validationResult
} }
private fun restoreCategories(jsonCategories: JsonElement) { // TODO private fun restoreCategories(jsonCategories: JsonElement) {
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories) val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
val dbCategories = getCategoryList() val dbCategories = getCategoryList()
// Iterate over them // Iterate over them and create missing categories
backupCategories.forEach { category -> backupCategories.forEach { category ->
if (dbCategories.none { it.name == category.name }) { if (dbCategories.none { it.name == category.name }) {
createCategory(category.name) createCategory(category.name)
@@ -24,7 +24,7 @@ object CachedImageResponse {
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? { private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file -> File(directoryPath).listFiles().forEach { file ->
if (file.name.startsWith(fileName)) if (file.name.startsWith("$fileName."))
return "$directoryPath/${file.name}" return "$directoryPath/${file.name}"
} }
return null return null
@@ -64,4 +64,11 @@ object CachedImageResponse {
throw Exception("request error! ${response.code}") throw Exception("request error! ${response.code}")
} }
} }
suspend fun clearCachedImage(saveDir: String, fileName: String) {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
cachedFile?.also {
File(it).delete()
}
}
} }
@@ -1,12 +1,5 @@
package ir.armor.tachidesk.model.database.migration package ir.armor.tachidesk.model.database.migration
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.model.database.migration.lib.Migration
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
@@ -14,6 +7,13 @@ import org.jetbrains.exposed.sql.transactions.transaction
* 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 eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.model.database.migration.lib.Migration
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
class M0001_Initial : Migration() { class M0001_Initial : Migration() {
private object ExtensionTable : IntIdTable() { private object ExtensionTable : IntIdTable() {
val apkName = varchar("apk_name", 1024) val apkName = varchar("apk_name", 1024)
@@ -100,6 +100,7 @@ class M0001_Initial : Migration() {
val manga = reference("manga", ir.armor.tachidesk.model.database.table.MangaTable) val manga = reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
} }
/** initial migration, create all tables */
override fun run() { override fun run() {
transaction { transaction {
SchemaUtils.create( SchemaUtils.create(
@@ -0,0 +1,23 @@
package ir.armor.tachidesk.model.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 ir.armor.tachidesk.model.database.migration.lib.Migration
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
class M0002_ChapterTableIndexRename : Migration() {
/** this migration renamed ChapterTable.NUMBER_IN_LIST to ChapterTable.INDEX */
override fun run() {
with(TransactionManager.current()) {
exec("ALTER TABLE CHAPTER ALTER COLUMN NUMBER_IN_LIST RENAME TO INDEX")
commit()
currentDialect.resetCaches()
}
}
}
@@ -7,7 +7,9 @@ package ir.armor.tachidesk.model.database.table
* 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 ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
val url = varchar("url", 2048) val url = varchar("url", 2048)
@@ -20,7 +22,22 @@ object ChapterTable : IntIdTable() {
val isBookmarked = bool("bookmark").default(false) val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0) val lastPageRead = integer("last_page_read").default(0)
val chapterIndex = integer("number_in_list") // index is reserved by a function
val chapterIndex = integer("index")
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
ChapterDataClass(
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
chapterEntry[ChapterTable.manga].value,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.chapterIndex],
)
@@ -10,13 +10,22 @@ package ir.armor.tachidesk.model.dataclass
data class ChapterDataClass( data class ChapterDataClass(
val url: String, val url: String,
val name: String, val name: String,
val date_upload: Long, val uploadDate: Long,
val chapter_number: Float, val chapterNumber: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, val mangaId: Int,
/** this chapter's index */ /** chapter is read */
val chapterIndex: Int? = null, val read: Boolean,
/** chapter is bookmarked */
val bookmarked: Boolean,
/** last read page, zero means not read/no data */
val lastPageRead: Int,
/** this chapter's index, starts with 1 */
val index: Int? = null,
/** total chapter count, used to calculate if there's a next and prev chapter */ /** total chapter count, used to calculate if there's a next and prev chapter */
val chapterCount: Int? = null, val chapterCount: Int? = null,
@@ -25,7 +25,9 @@ data class MangaDataClass(
val genre: String? = null, val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name, val status: String = MangaStatus.UNKNOWN.name,
val inLibrary: Boolean = false, val inLibrary: Boolean = false,
val source: SourceDataClass? = null val source: SourceDataClass? = null,
val freshData: Boolean = false
) )
data class PagedMangaListDataClass( data class PagedMangaListDataClass(
@@ -13,6 +13,7 @@ import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
import ir.armor.tachidesk.impl.Chapter.getChapter import ir.armor.tachidesk.impl.Chapter.getChapter
import ir.armor.tachidesk.impl.Chapter.getChapterList import ir.armor.tachidesk.impl.Chapter.getChapterList
import ir.armor.tachidesk.impl.Chapter.modifyChapter
import ir.armor.tachidesk.impl.Extension.getExtensionIcon import ir.armor.tachidesk.impl.Extension.getExtensionIcon
import ir.armor.tachidesk.impl.Extension.installExtension import ir.armor.tachidesk.impl.Extension.installExtension
import ir.armor.tachidesk.impl.Extension.uninstallExtension import ir.armor.tachidesk.impl.Extension.uninstallExtension
@@ -102,6 +103,7 @@ object JavalinSetup {
ctx.result(e.message ?: "Internal Server Error") ctx.result(e.message ?: "Internal Server Error")
} }
// list all extensions
app.get("/api/v1/extension/list") { ctx -> app.get("/api/v1/extension/list") { ctx ->
ctx.json( ctx.json(
future { future {
@@ -110,6 +112,7 @@ object JavalinSetup {
) )
} }
// install extension identified with "pkgName"
app.get("/api/v1/extension/install/:pkgName") { ctx -> app.get("/api/v1/extension/install/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
@@ -120,6 +123,7 @@ object JavalinSetup {
) )
} }
// update extension identified with "pkgName"
app.get("/api/v1/extension/update/:pkgName") { ctx -> app.get("/api/v1/extension/update/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
@@ -130,6 +134,7 @@ object JavalinSetup {
) )
} }
// uninstall extension identified with "pkgName"
app.get("/api/v1/extension/uninstall/:pkgName") { ctx -> app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
@@ -138,7 +143,7 @@ object JavalinSetup {
} }
// icon for extension named `apkName` // icon for extension named `apkName`
app.get("/api/v1/extension/icon/:apkName") { ctx -> app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
ctx.result( ctx.result(
@@ -186,9 +191,11 @@ object JavalinSetup {
// get manga info // get manga info
app.get("/api/v1/manga/:mangaId/") { ctx -> app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json( ctx.json(
future { future {
getManga(mangaId) getManga(mangaId, onlineFetch)
} }
) )
} }
@@ -249,7 +256,10 @@ object JavalinSetup {
// get chapter list when showing a manga // get chapter list when showing a manga
app.get("/api/v1/manga/:mangaId/chapters") { ctx -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(future { getChapterList(mangaId) })
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json(future { getChapterList(mangaId, onlineFetch) })
} }
// used to display a chapter, get a chapter in order to show it's pages // used to display a chapter, get a chapter in order to show it's pages
@@ -259,6 +269,22 @@ object JavalinSetup {
ctx.json(future { getChapter(chapterIndex, mangaId) }) ctx.json(future { getChapter(chapterIndex, mangaId) })
} }
// used to modify a chapter's parameters
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
val read = ctx.formParam("read")?.toBoolean()
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
ctx.status(200)
}
// get page at index "index"
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
@@ -273,7 +299,7 @@ object JavalinSetup {
) )
} }
// global search // global search, Not implemented yet
app.get("/api/v1/search/:searchTerm") { ctx -> app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm)) ctx.json(sourceGlobalSearch(searchTerm))
@@ -109,7 +109,6 @@ fun applicationSetup() {
// socks proxy settings // socks proxy settings
if (serverConfig.socksProxyEnabled) { if (serverConfig.socksProxyEnabled) {
// System.getProperties()["proxySet"] = "true"
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
+17 -18
View File
@@ -3,21 +3,19 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@material-ui/core": "^4.11.2", "@fontsource/roboto": "^4.3.0",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.4", "@material-ui/lab": "^4.0.0-alpha.58",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/react-lazyload": "^3.1.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"file-selector": "^0.2.4", "file-selector": "^0.2.4",
"fontsource-roboto": "^4.0.0", "react": "^17.0.2",
"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.2",
"react-lazyload": "^3.2.0", "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.3",
"react-virtuoso": "^1.8.6",
"web-vitals": "^0.2.4" "web-vitals": "^0.2.4"
}, },
"scripts": { "scripts": {
@@ -39,17 +37,18 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^17.0.0", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.2",
"@types/react-router-dom": "^5.1.6", "@types/react-lazyload": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^4.11.0", "@types/react-router-dom": "^5.1.7",
"@typescript-eslint/parser": "4.11.0", "@typescript-eslint/eslint-plugin": "4.23.0",
"eslint": "^7.16.0", "@typescript-eslint/parser": "4.23.0",
"eslint-config-airbnb-typescript": "^12.0.0", "eslint": "^7.26.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"typescript": "^4.1.0" "typescript": "^4.2.4"
} }
} }
+79 -27
View File
@@ -7,12 +7,17 @@
* 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 from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles, useTheme } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; 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 IconButton from '@material-ui/core/IconButton';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import client from '../util/client';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -21,6 +26,9 @@ const useStyles = makeStyles((theme) => ({
alignItems: 'center', alignItems: 'center',
padding: 16, padding: 16,
}, },
read: {
backgroundColor: theme.palette.type === 'dark' ? '#353535' : '#f0f0f0',
},
bullet: { bullet: {
display: 'inline-block', display: 'inline-block',
margin: '0 2px', margin: '0 2px',
@@ -42,46 +50,90 @@ const useStyles = makeStyles((theme) => ({
interface IProps{ interface IProps{
chapter: IChapter chapter: IChapter
triggerChaptersUpdate: () => void
} }
export default function ChapterCard(props: IProps) { export default function ChapterCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
const history = useHistory(); const history = useHistory();
const { chapter } = props; const theme = useTheme();
const { chapter, triggerChaptersUpdate } = props;
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10); const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const sendChange = (key: string, value: any) => {
handleClose();
const formData = new FormData();
formData.append(key, value);
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
.then(() => triggerChaptersUpdate());
};
return ( return (
<> <>
<li> <li>
<Card> <Card>
<CardContent className={classes.root}> <CardContent className={`${classes.root} ${chapter.read && classes.read}`}>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{chapter.name}
{chapter.chapter_number > 0 && ` : ${chapter.chapter_number}`}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{chapter.scanlator}
{chapter.scanlator && ' '}
{dateStr}
</Typography>
</div>
</div>
<Link <Link
to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`} to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
style={{ textDecoration: 'none' }} style={{
textDecoration: 'none',
color: theme.palette.text.primary,
}}
> >
<Button <div style={{ display: 'flex' }}>
variant="outlined" <div style={{ display: 'flex', flexDirection: 'column' }}>
style={{ marginLeft: 20 }} <Typography variant="h5" component="h2">
> <span style={{ color: theme.palette.primary.dark }}>
open {chapter.bookmarked && <BookmarkIcon />}
</span>
</Button> {chapter.name}
{chapter.chapterNumber > 0 && ` : ${chapter.chapterNumber}`}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{chapter.scanlator}
{chapter.scanlator && ' '}
{dateStr}
</Typography>
</div>
</div>
</Link> </Link>
<IconButton aria-label="more" onClick={handleClick}>
<MoreVertIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{/* <MenuItem onClick={handleClose}>Download</MenuItem> */}
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
{chapter.bookmarked && 'Remove bookmark'}
{!chapter.bookmarked && 'Bookmark'}
</MenuItem>
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
Mark as
{' '}
{chapter.read && 'unread'}
{!chapter.read && 'read'}
</MenuItem>
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
Mark previous as Read
</MenuItem>
</Menu>
</CardContent> </CardContent>
</Card> </Card>
</li> </li>
+1 -1
View File
@@ -198,7 +198,7 @@ export default function MangaDetails(props: IProps) {
<div className={classes.top}> <div className={classes.top}>
<div className={classes.leftRight}> <div className={classes.leftRight}>
<div className={classes.leftSide}> <div className={classes.leftSide}>
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" /> <img src={`${serverAddress}${manga.thumbnailUrl}?x=${Math.random()}`} alt="Manga Thumbnail" />
</div> </div>
<div className={classes.rightSide}> <div className={classes.rightSide}>
<h1> <h1>
+103 -104
View File
@@ -23,6 +23,8 @@ import { Switch } from '@material-ui/core';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemIcon from '@material-ui/core/ListItemIcon';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Collapse from '@material-ui/core/Collapse'; import Collapse from '@material-ui/core/Collapse';
@@ -137,16 +139,11 @@ const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
}, },
})); }));
export interface IReaderSettings{
staticNav: boolean
showPageNumber: boolean
continuesPageGap: boolean
}
export const defaultReaderSettings = () => ({ export const defaultReaderSettings = () => ({
staticNav: false, staticNav: false,
showPageNumber: true, showPageNumber: true,
continuesPageGap: false, continuesPageGap: false,
readerType: 'ContinuesVertical',
} as IReaderSettings); } as IReaderSettings);
interface IProps { interface IProps {
@@ -171,7 +168,7 @@ export default function ReaderNavBar(props: IProps) {
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav); const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
const [hideOpenButton, setHideOpenButton] = useState(false); const [hideOpenButton, setHideOpenButton] = useState(false);
const [prevScrollPos, setPrevScrollPos] = useState(0); const [prevScrollPos, setPrevScrollPos] = useState(0);
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(false); const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(true);
const theme = useTheme(); const theme = useTheme();
const classes = useStyles(settings)(); const classes = useStyles(settings)();
@@ -205,32 +202,31 @@ export default function ReaderNavBar(props: IProps) {
return ( return (
<> <>
<ClickAwayListener onClickAway={() => (drawerVisible && setDrawerOpen(false))}> <Slide
<Slide direction="right"
direction="right" in={drawerOpen}
in={drawerOpen} timeout={200}
timeout={200} appear={false}
appear={false} mountOnEnter
mountOnEnter unmountOnExit
unmountOnExit onEntered={() => setDrawerVisible(true)}
onEntered={() => setDrawerVisible(true)} onExited={() => setDrawerVisible(false)}
onExited={() => setDrawerVisible(false)} >
> <div className={classes.root}>
<div className={classes.root}> <header>
<header> <IconButton
<IconButton edge="start"
edge="start" color="inherit"
color="inherit" aria-label="menu"
aria-label="menu" disableRipple
disableRipple onClick={() => history.push(`/manga/${manga.id}`)}
onClick={() => history.push(`/manga/${manga.id}`)} >
> <CloseIcon />
<CloseIcon /> </IconButton>
</IconButton> <Typography variant="h1">
<Typography variant="h1"> {title}
{title} </Typography>
</Typography> {!settings.staticNav
{!settings.staticNav
&& ( && (
<IconButton <IconButton
edge="start" edge="start"
@@ -242,74 +238,78 @@ export default function ReaderNavBar(props: IProps) {
<KeyboardArrowLeftIcon /> <KeyboardArrowLeftIcon />
</IconButton> </IconButton>
) } ) }
</header> </header>
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}> <ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
<ListItemText primary="Reader Settings" /> <ListItemText primary="Reader Settings" />
<ListItemSecondaryAction> <ListItemSecondaryAction>
<IconButton <IconButton
edge="start" edge="start"
color="inherit" color="inherit"
aria-label="menu" aria-label="menu"
disableRipple disableRipple
disableFocusRipple disableFocusRipple
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)} 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>
<ListItem>
<ListItemText primary="Reader Type" />
<Select
value={settings.readerType}
onChange={(e) => setSettingValue('readerType', e.target.value)}
> >
{settingsCollapseOpen && <KeyboardArrowUpIcon />} <MenuItem value="SingleLTR">Left to right</MenuItem>
{!settingsCollapseOpen && <KeyboardArrowDownIcon />} <MenuItem value="SingleRTL">Right to left(WIP)</MenuItem>
</IconButton> <MenuItem value="SingleVertical">Vertical(WIP)</MenuItem>
</ListItemSecondaryAction> <MenuItem value="Webtoon">Webtoon</MenuItem>
</ListItem> <MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit> <MenuItem value="ContinuesHorizontal">Horizontal(WIP)</MenuItem>
<List> </Select>
<ListItem> </ListItem>
<ListItemText primary="Static Navigation" /> </List>
<ListItemSecondaryAction> </Collapse>
<Switch <hr />
edge="end" <div className={classes.navigation}>
checked={settings.staticNav} <span>
onChange={(e) => setSettingValue('staticNav', e.target.checked)} Currently on page
/> {' '}
</ListItemSecondaryAction> {curPage + 1}
</ListItem> {' '}
<ListItem> of
<ListItemText primary="Show page number" /> {' '}
<ListItemSecondaryAction> {chapter.pageCount}
<Switch </span>
edge="end" <div className={classes.navigationChapters}>
checked={settings.showPageNumber} {chapter.index > 1
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Continues Page gap" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.continuesPageGap}
onChange={(e) => setSettingValue('continuesPageGap', 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 <Link
style={{ gridArea: 'prev' }} style={{ gridArea: 'prev' }}
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex - 1}`} to={`/manga/${manga.id}/chapter/${chapter.index - 1}`}
> >
<Button <Button
variant="outlined" variant="outlined"
@@ -317,15 +317,15 @@ export default function ReaderNavBar(props: IProps) {
> >
Chapter Chapter
{' '} {' '}
{chapter.chapterIndex - 1} {chapter.index - 1}
</Button> </Button>
</Link> </Link>
)} )}
{chapter.chapterIndex < chapter.chapterCount {chapter.index < chapter.chapterCount
&& ( && (
<Link <Link
style={{ gridArea: 'next' }} style={{ gridArea: 'next' }}
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex + 1}`} to={`/manga/${manga.id}/chapter/${chapter.index + 1}`}
> >
<Button <Button
variant="outlined" variant="outlined"
@@ -333,15 +333,14 @@ export default function ReaderNavBar(props: IProps) {
> >
Chapter Chapter
{' '} {' '}
{chapter.chapterIndex + 1} {chapter.index + 1}
</Button> </Button>
</Link> </Link>
)} )}
</div>
</div> </div>
</div> </div>
</Slide> </div>
</ClickAwayListener> </Slide>
<Zoom in={!drawerOpen}> <Zoom in={!drawerOpen}>
<Fade in={!hideOpenButton}> <Fade in={!hideOpenButton}>
<IconButton <IconButton
@@ -24,7 +24,7 @@ const useStyles = makeStyles({
interface IProps { interface IProps {
drawerOpen: boolean drawerOpen: boolean
setDrawerOpen(state: boolean): void setDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
} }
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) { export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
+62
View File
@@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import ReactDOM from 'react-dom';
import React from 'react';
import Slide, { SlideProps } from '@material-ui/core/Slide';
import Snackbar from '@material-ui/core/Snackbar';
import MuiAlert, { AlertProps, Color as Severity } from '@material-ui/lab/Alert';
function removeToast(id: string) {
const container = document.querySelector(`#${id}`)!!;
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
}
function Transition(props: SlideProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <Slide {...props} direction="up" />;
}
function Alert(props: AlertProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
interface IToastProps{
message: string
severity: Severity
}
function Toast(props: IToastProps) {
const { message, severity } = props;
const [open, setOpen] = React.useState(true);
const handleClose = () => {
setOpen(false);
};
return (
<Snackbar
open={open}
onClose={handleClose}
autoHideDuration={3000}
TransitionComponent={Transition}
message="I love snacks"
>
<MuiAlert elevation={6} variant="filled" onClose={handleClose} severity={severity}>
{message}
</MuiAlert>
</Snackbar>
);
}
export default function makeToast(message: string, severity: Severity) {
const id = Math.floor(Math.random() * 1000);
const container = document.createElement('div');
container.id = `alert-${id}`;
document.body.appendChild(container);
ReactDOM.render(<Toast message={message} severity={severity} />, container);
setTimeout(() => removeToast(container.id), 3500);
}
@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Page from './Page';
const useStyles = makeStyles({
reader: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0 auto',
width: '100%',
height: '100vh',
overflowX: 'scroll',
},
});
interface IProps {
pages: Array<IReaderPage>
setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings
}
export default function HorizontalReader(props: IProps) {
const { pages, settings, setCurPage } = props;
const classes = useStyles();
return (
<div className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
setCurPage={setCurPage}
settings={settings}
/>
))
}
</div>
);
}
@@ -11,23 +11,26 @@ import CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import LazyLoad from 'react-lazyload'; import LazyLoad from 'react-lazyload';
import { IReaderSettings } from './ReaderNavBar';
const useStyles = (settings: IReaderSettings) => makeStyles({ const useStyles = (settings: IReaderSettings) => makeStyles({
loading: { loading: {
margin: '100px auto', margin: '100px auto',
height: '100vh', height: '100vh',
width: '100vw',
}, },
loadingImage: { loadingImage: {
padding: settings.staticNav ? 'calc(50vh - 40px) calc(50vw - 340px)' : 'calc(50vh - 40px) calc(50vw - 40px)',
height: '100vh', height: '100vh',
width: '200px', width: '70vw',
padding: '50px calc(50% - 20px)',
backgroundColor: '#525252', backgroundColor: '#525252',
marginBottom: 10, marginBottom: 10,
}, },
image: { image: {
display: 'block', display: 'block',
marginBottom: settings.continuesPageGap ? '15px' : 0, marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
minWidth: '50vw',
width: '100%',
maxWidth: '100%',
}, },
}); });
@@ -73,7 +76,7 @@ function LazyImage(props: IProps) {
if (imageSrc.length === 0) { if (imageSrc.length === 0) {
return ( return (
<div className={classes.loadingImage}> <div className={`${classes.image} ${classes.loadingImage}`}>
<CircularProgress thickness={5} /> <CircularProgress thickness={5} />
</div> </div>
); );
@@ -85,7 +88,6 @@ function LazyImage(props: IProps) {
ref={ref} ref={ref}
src={imageSrc} src={imageSrc}
alt={`Page #${index}`} alt={`Page #${index}`}
style={{ width: '100%' }}
/> />
); );
} }
@@ -98,22 +100,12 @@ export default function Page(props: IProps) {
return ( return (
<div style={{ margin: '0 auto' }}> <div style={{ margin: '0 auto' }}>
<LazyLoad <LazyImage
offset={window.innerHeight} src={src}
placeholder={( index={index}
<div className={classes.loading}> setCurPage={setCurPage}
<CircularProgress thickness={5} /> settings={settings}
</div> />
)}
once
>
<LazyImage
src={src}
index={index}
setCurPage={setCurPage}
settings={settings}
/>
</LazyLoad>
</div> </div>
); );
} }
@@ -0,0 +1,32 @@
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
const useStyles = (settings: IReaderSettings) => makeStyles({
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',
},
});
interface IProps {
settings: IReaderSettings
curPage: number
pageCount: number
}
export default function PageNumber(props: IProps) {
const { settings, curPage, pageCount } = props;
const classes = useStyles(settings)();
return (
<div className={classes.pageNumber}>
{`${curPage + 1} / ${pageCount}`}
</div>
);
}
@@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { makeStyles } from '@material-ui/core/styles';
import React, { useEffect } from 'react';
import Page from './Page';
const useStyles = makeStyles({
reader: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0 auto',
width: '100%',
height: '100vh',
},
});
export default function PagedReader(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage,
} = props;
const classes = useStyles();
function nextPage() {
if (curPage < pages.length - 1) { setCurPage(curPage + 1); }
}
function prevPage() {
if (curPage > 0) { setCurPage(curPage - 1); }
}
function keyboardControl(e:KeyboardEvent) {
switch (e.key) {
case 'ArrowRight':
nextPage();
break;
case 'ArrowLeft':
prevPage();
break;
default:
break;
}
}
function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
nextPage();
} else {
prevPage();
}
}
useEffect(() => {
document.addEventListener('keyup', keyboardControl, false);
document.addEventListener('click', clickControl);
return () => {
document.removeEventListener('keyup', keyboardControl);
document.removeEventListener('click', clickControl);
};
}, [curPage]);
return (
<div className={classes.reader}>
<Page
key={curPage}
index={curPage}
src={pages[curPage].src}
setCurPage={setCurPage}
settings={settings}
/>
</div>
);
}
@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Page from './Page';
const useStyles = makeStyles({
reader: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
width: '100%',
},
});
export default function VerticalReader(props: IReaderProps) {
const { pages, settings, setCurPage } = props;
const classes = useStyles();
return (
<div className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
setCurPage={setCurPage}
settings={settings}
/>
))
}
</div>
);
}
+1 -1
View File
@@ -10,7 +10,7 @@ import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import './index.css'; import './index.css';
// roboto font // roboto font
import 'fontsource-roboto'; import '@fontsource/roboto';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
+58 -23
View File
@@ -7,14 +7,15 @@
* 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 { makeStyles, Theme, useTheme } 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 { Virtuoso } from 'react-virtuoso';
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';
import LoadingPlaceholder from '../components/LoadingPlaceholder'; import LoadingPlaceholder from '../components/LoadingPlaceholder';
import makeToast from '../components/Toast';
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
root: { root: {
@@ -26,6 +27,8 @@ const useStyles = makeStyles((theme: Theme) => ({
chapters: { chapters: {
listStyle: 'none', listStyle: 'none',
padding: 0, padding: 0,
width: '100vw',
minHeight: '200px',
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
width: '50vw', width: '50vw',
height: 'calc(100vh - 64px)', height: 'calc(100vh - 64px)',
@@ -41,41 +44,58 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
})); }));
// const InnerItem = React.memo(({ chapters, index }: any) => (
// <ChapterCard chapter={chapters[index]} />
// ));
export default function Manga() { export default function Manga() {
const classes = useStyles(); const classes = useStyles();
const theme = useTheme();
const { setTitle } = useContext(NavbarContext); const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails 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 [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
const [fetchedChapters, setFetchedChapters] = useState(false);
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
function triggerChaptersUpdate() {
setChapterUpdateTriggerer(chapterUpdateTriggerer + 1);
}
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/`) if (manga === undefined || !manga.freshData) {
.then((response) => response.data) client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
.then((data: IManga) => { .then((response) => response.data)
setManga(data); .then((data: IManga) => {
setTitle(data.title); setManga(data);
}); setTitle(data.title);
}, []); });
}
}, [manga]);
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/chapters`) const shouldFetchOnline = fetchedChapters && chapterUpdateTriggerer === 0;
client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`)
.then((response) => response.data) .then((response) => response.data)
.then((data) => setChapters(data)); .then((data) => {
}, []); if (data.length === 0 && fetchedChapters) {
makeToast('No chapters found', 'warning');
const chapterCards = ( }
<LoadingPlaceholder setChapters(data);
shouldRender={chapters.length > 0} })
> .then(() => setFetchedChapters(true));
<ol className={classes.chapters}> }, [chapters.length, fetchedChapters, chapterUpdateTriggerer]);
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
</ol>
</LoadingPlaceholder>
// const itemContent = (index:any) => <InnerItem chapters={chapters} index={index} />;
const itemContent = (index:any) => (
<ChapterCard
chapter={chapters[index]}
triggerChaptersUpdate={triggerChaptersUpdate}
/>
); );
return ( return (
@@ -85,7 +105,22 @@ export default function Manga() {
component={MangaDetails} component={MangaDetails}
componentProps={{ manga }} componentProps={{ manga }}
/> />
{chapterCards}
<LoadingPlaceholder
shouldRender={chapters.length > 0 || fetchedChapters}
>
<Virtuoso
style={{ // override Virtuoso default values and set them with class
height: 'undefined',
}}
className={classes.chapters}
totalCount={chapters.length}
itemContent={itemContent}
useWindowScroll={window.innerWidth < 960}
overscan={window.innerHeight * 0.5}
/>
</LoadingPlaceholder>
</div> </div>
); );
} }
+56 -33
View File
@@ -10,38 +10,53 @@ import CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles'; 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 HorizontalReader from '../components/reader/HorizontalReader';
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar'; import Page from '../components/reader/Page';
import PageNumber from '../components/reader/PageNumber';
import PagedReader from '../components/reader/PagedReader';
import VerticalReader from '../components/reader/VerticalReader';
import ReaderNavBar, { defaultReaderSettings } 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 useStyles = (settings: IReaderSettings) => makeStyles({ const useStyles = (settings: IReaderSettings) => makeStyles({
reader: { root: {
display: 'flex', width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
}, },
loading: { loading: {
margin: '50px auto', 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 getReaderComponent = (readerType: ReaderType) => {
switch (readerType) {
case 'ContinuesVertical':
return VerticalReader;
break;
case 'Webtoon':
return VerticalReader;
break;
case 'SingleVertical':
return PagedReader;
break;
case 'SingleRTL':
return PagedReader;
break;
case 'SingleLTR':
return PagedReader;
break;
case 'ContinuesHorizontal':
return HorizontalReader;
default:
return VerticalReader;
break;
}
};
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 }); const initialChapter = () => ({ pageCount: -1, index: -1, chapterCount: 0 });
export default function Reader() { export default function Reader() {
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings); const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
@@ -50,7 +65,7 @@ export default function Reader() {
const [serverAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const { chapterIndex, mangaId } = useParams<{chapterIndex: string, mangaId: string}>(); const { chapterIndex, mangaId } = useParams<{ chapterIndex: string, mangaId: string }>();
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' }); const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter()); const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
const [curPage, setCurPage] = useState<number>(0); const [curPage, setCurPage] = useState<number>(0);
@@ -102,20 +117,28 @@ export default function Reader() {
</div> </div>
); );
} }
const pages = range(chapter.pageCount).map((index) => ({
index,
src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`,
}));
const ReaderComponent = getReaderComponent(settings.readerType);
return ( return (
<div className={classes.reader}> <div className={classes.root}>
<div className={classes.pageNumber}> <PageNumber
{`${curPage + 1} / ${chapter.pageCount}`} settings={settings}
</div> curPage={curPage}
{range(chapter.pageCount).map((index) => ( pageCount={chapter.pageCount}
<Page />
key={index} <ReaderComponent
index={index} pages={pages}
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`} pageCount={chapter.pageCount}
setCurPage={setCurPage} setCurPage={setCurPage}
settings={settings} curPage={curPage}
/> settings={settings}
))} />
</div> </div>
); );
} }
+1 -1
View File
@@ -27,7 +27,7 @@ export default function SearchSingle() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Search'); setAction(<></>); }, []); useEffect(() => { setTitle('Search'); setAction(<></>); }, []);
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<IMangaCard[]>([]); const [mangas, setMangas] = useState<IMangaCard[]>([]);
+1 -1
View File
@@ -15,7 +15,7 @@ export default function SourceMangas(props: { popular: boolean }) {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Source'); setAction(<></>); }, []); useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{ sourceId: string }>();
const [mangas, setMangas] = useState<IMangaCard[]>([]); 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);
+36 -4
View File
@@ -50,24 +50,29 @@ interface IManga {
inLibrary: boolean inLibrary: boolean
source: ISource source: ISource
freshData: boolean
} }
interface IChapter { interface IChapter {
id: number id: number
url: string url: string
name: string name: string
date_upload: number uploadDate: number
chapter_number: number chapterNumber: number
scanlator: String scanlator: String
mangaId: number mangaId: number
chapterIndex: number read: boolean
bookmarked: boolean
lastPageRead: number
index: number
chapterCount: number chapterCount: number
pageCount: number pageCount: number
} }
interface IPartialChpter { interface IPartialChpter {
pageCount: number pageCount: number
chapterIndex: number index: number
chapterCount: number chapterCount: number
} }
@@ -82,3 +87,30 @@ interface INavbarOverride {
status: boolean status: boolean
value: any value: any
} }
type ReaderType =
'ContinuesVertical'|
'Webtoon' |
'SingleVertical' |
'SingleRTL' |
'SingleLTR' |
'ContinuesHorizontal';
interface IReaderSettings{
staticNav: boolean
showPageNumber: boolean
readerType: ReaderType
}
interface IReaderPage {
index: number
src: string
}
interface IReaderProps {
pages: Array<IReaderPage>
pageCount: number
setCurPage: React.Dispatch<React.SetStateAction<number>>
curPage: number
settings: IReaderSettings
}
+1822 -1780
View File
File diff suppressed because it is too large Load Diff