mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-05 11:54:38 -05:00
Compare commits
20 Commits
v0.2.4
...
v0.2.6-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f41c5c9428 | ||
|
|
04837983fa | ||
|
|
5d484b012c | ||
|
|
436a8d0585 | ||
|
|
28cc0a6f84 | ||
|
|
26cc2f2c96 | ||
|
|
149107e749 | ||
|
|
a74936c5f5 | ||
|
|
ff8c8913d4 | ||
|
|
83426e1302 | ||
|
|
9cd93d467c | ||
|
|
257f8a5a27 | ||
|
|
79bab08cae | ||
|
|
4e699e4f5a | ||
|
|
1128f40bac | ||
|
|
53ef836326 | ||
|
|
b8df0e89e5 | ||
|
|
472bfec6bf | ||
|
|
c1b86cedd2 | ||
|
|
428c65f075 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -34,10 +34,10 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
|||||||
2. Second Step
|
2. Second Step
|
||||||
|
|
||||||
### Expected behavior
|
### Expected behavior
|
||||||
Describe what should have happened
|
Describe what should have happened. Remove this line after you are done.
|
||||||
|
|
||||||
### Actual behavior
|
### Actual behavior
|
||||||
Describe what happens instead
|
Describe what happens instead. Remove this line after you are done.
|
||||||
|
|
||||||
## Other details
|
## Other details
|
||||||
Describe additional details If necessary
|
Describe additional details If necessary. Remove this line after you are done.
|
||||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -23,7 +23,7 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
|||||||
---
|
---
|
||||||
|
|
||||||
## What feature should be added to Tachidesk?
|
## What feature should be added to Tachidesk?
|
||||||
Explain What the feature is and how it should work in detail
|
Explain What the feature is and how it should work in detail. Remove this line after you are done.
|
||||||
|
|
||||||
## Why/Project's Benefit/Existing Problem
|
## Why/Project's Benefit/Existing Problem
|
||||||
Explain why this should be added
|
Explain why this should be added. Remove this line after you are done.
|
||||||
5
.github/workflows/issue_closer.yml
vendored
5
.github/workflows/issue_closer.yml
vendored
@@ -28,5 +28,10 @@ jobs:
|
|||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||||
"message": "The requested information was not filled out"
|
"message": "The requested information was not filled out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*Remove this line after you are done.*",
|
||||||
|
"message": "The lines requesting to be removed were not removed."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||||
|
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
echo "Getting required Android.jar..."
|
echo "Getting required Android.jar..."
|
||||||
rm -rf "tmp"
|
rm -rf "tmp"
|
||||||
mkdir -p "tmp"
|
mkdir -p "tmp"
|
||||||
28
README.md
28
README.md
@@ -21,19 +21,25 @@ Here is a list of current features:
|
|||||||
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
|
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
|
||||||
|
|
||||||
## Downloading and Running the app
|
## Downloading and Running the app
|
||||||
#### Prerequisites
|
### All Operating Systems
|
||||||
You should have The Java Runtime Environment(JRE) 8 or newer (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||||
|
|
||||||
#### Download the app
|
Download the latest jar release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||||
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
|
||||||
|
|
||||||
#### Running pre-built jar packages
|
|
||||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||||
|
|
||||||
#### Running pre-built Windows packages
|
### Windows
|
||||||
Windows specific builds have java bundled inside them, 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 will work like the jar release.
|
Download the latest win32 release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||||
|
|
||||||
#### Running on Docker
|
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
|
||||||
|
|
||||||
|
### Arch Linux
|
||||||
|
You can install Tachidesk from the AUR
|
||||||
|
```
|
||||||
|
yay -S tachidesk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||||
|
|
||||||
## General troubleshooting
|
## General troubleshooting
|
||||||
@@ -43,7 +49,7 @@ On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
|||||||
|
|
||||||
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
||||||
|
|
||||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Tachidesk`
|
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
||||||
|
|
||||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||||
|
|
||||||
@@ -60,7 +66,7 @@ This project has two components:
|
|||||||
#### Manual download
|
#### Manual download
|
||||||
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
||||||
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
Run `AndroidCompat/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
||||||
### building the jar
|
### building the jar
|
||||||
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
### building the Windows package
|
### building the Windows package
|
||||||
@@ -76,7 +82,7 @@ How to do it is described in `webUI/react/README.md` but for short,
|
|||||||
and supports HMR and all the other goodies you'll need.
|
and supports HMR and all the other goodies you'll need.
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ plugins {
|
|||||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.2.4"
|
val TachideskVersion = "v0.2.6"
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -63,7 +63,7 @@ dependencies {
|
|||||||
val coroutinesVersion = "1.3.9"
|
val coroutinesVersion = "1.3.9"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
|
||||||
// dex2jar
|
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||||
implementation(fileTree("lib/dex2jar/"))
|
implementation(fileTree("lib/dex2jar/"))
|
||||||
|
|
||||||
// api
|
// api
|
||||||
@@ -88,8 +88,8 @@ dependencies {
|
|||||||
implementation(project(":AndroidCompat:Config"))
|
implementation(project(":AndroidCompat:Config"))
|
||||||
|
|
||||||
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
// testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
// testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = "ir.armor.tachidesk.Main"
|
val name = "ir.armor.tachidesk.Main"
|
||||||
|
|||||||
BIN
server/lib/dex2jar/ST4-4.0.8.jar
Normal file
BIN
server/lib/dex2jar/ST4-4.0.8.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/antlr-3.5.2.jar
Normal file
BIN
server/lib/dex2jar/antlr-3.5.2.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/antlr4-4.5.jar
Normal file
BIN
server/lib/dex2jar/antlr4-4.5.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/antlr4-runtime-4.5.jar
Normal file
BIN
server/lib/dex2jar/antlr4-runtime-4.5.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/asm-debug-all-5.0.3.jar
Normal file
BIN
server/lib/dex2jar/asm-debug-all-5.0.3.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/d2j-base-cmd-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/d2j-base-cmd-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/d2j-jasmin-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/d2j-jasmin-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/d2j-smali-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/d2j-smali-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dex-ir-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-ir-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dex-reader-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-reader-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dex-reader-api-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-reader-api-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dex-tools-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-tools-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dex-translator-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-translator-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dex-writer-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-writer-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/lib/dex2jar/dx-27.0.3.jar
Normal file
BIN
server/lib/dex2jar/dx-27.0.3.jar
Normal file
Binary file not shown.
67
server/lib/dex2jar/open-source-license.txt
Normal file
67
server/lib/dex2jar/open-source-license.txt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
==== dx-*.jar
|
||||||
|
Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
|
||||||
|
==== antlr-*.jar
|
||||||
|
[The BSD License]
|
||||||
|
Copyright (c) 2003-2007, Terence Parr
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of the author nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
==== asm-*.jar
|
||||||
|
|
||||||
|
ASM: a very small and fast Java bytecode manipulation framework
|
||||||
|
Copyright (c) 2000-2005 INRIA, France Telecom
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. Neither the name of the copyright holders nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||||
|
THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
BIN
server/lib/dex2jar/org.abego.treelayout.core-1.0.1.jar
Normal file
BIN
server/lib/dex2jar/org.abego.treelayout.core-1.0.1.jar
Normal file
Binary file not shown.
@@ -58,6 +58,10 @@ class Main {
|
|||||||
openInBrowser()
|
openInBrowser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.exception(NullPointerException::class.java) { _, ctx ->
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/v1/extension/list") { ctx ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
ctx.json(getExtensionList())
|
ctx.json(getExtensionList())
|
||||||
}
|
}
|
||||||
@@ -78,6 +82,7 @@ class Main {
|
|||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// icon for extension named `apkName`
|
||||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||||
val apkName = ctx.pathParam("apkName")
|
val apkName = ctx.pathParam("apkName")
|
||||||
val result = getExtensionIcon(apkName)
|
val result = getExtensionIcon(apkName)
|
||||||
@@ -86,31 +91,38 @@ class Main {
|
|||||||
ctx.header("content-type", result.second)
|
ctx.header("content-type", result.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// list of sources
|
||||||
app.get("/api/v1/source/list") { ctx ->
|
app.get("/api/v1/source/list") { ctx ->
|
||||||
ctx.json(getSourceList())
|
ctx.json(getSourceList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetch source with id `sourceId`
|
||||||
app.get("/api/v1/source/:sourceId") { ctx ->
|
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
ctx.json(getSource(sourceId))
|
ctx.json(getSource(sourceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// popular mangas from source with id `sourceId`
|
||||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// latest mangas from source with id `sourceId`
|
||||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
ctx.json(getManga(mangaId))
|
ctx.json(getManga(mangaId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manga thumbnail
|
||||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
val result = getThumbnail(mangaId)
|
val result = getThumbnail(mangaId)
|
||||||
@@ -133,7 +145,7 @@ class Main {
|
|||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adds the manga to category
|
// list manga's categories
|
||||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getMangaCategories(mangaId))
|
ctx.json(getMangaCategories(mangaId))
|
||||||
@@ -215,7 +227,7 @@ class Main {
|
|||||||
|
|
||||||
// category modification
|
// category modification
|
||||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId")!!.toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
val name = ctx.formParam("name")
|
val name = ctx.formParam("name")
|
||||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||||
updateCategory(categoryId, name, isLanding)
|
updateCategory(categoryId, name, isLanding)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ir.armor.tachidesk.database.table.MangaStatus
|
|||||||
|
|
||||||
data class MangaDataClass(
|
data class MangaDataClass(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val sourceId: Long,
|
val sourceId: String,
|
||||||
|
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
@@ -21,7 +21,8 @@ data class MangaDataClass(
|
|||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PagedMangaListDataClass(
|
data class PagedMangaListDataClass(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ package ir.armor.tachidesk.database.dataclass
|
|||||||
|
|
||||||
data class SourceDataClass(
|
data class SourceDataClass(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String?,
|
||||||
val lang: String,
|
val lang: String?,
|
||||||
val iconUrl: String,
|
val iconUrl: String?,
|
||||||
val supportsLatest: Boolean
|
val supportsLatest: Boolean?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ object MangaTable : IntIdTable() {
|
|||||||
val defaultCategory = bool("default_category").default(true)
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
// source is used by some ancestor of IntIdTable
|
// source is used by some ancestor of IntIdTable
|
||||||
val sourceReference = reference("source", SourceTable)
|
val sourceReference = long("source")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaEntry[MangaTable.id].value,
|
mangaEntry[MangaTable.id].value,
|
||||||
mangaEntry[sourceReference].value,
|
mangaEntry[sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
|
|
||||||
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||||
val mangaDetails = getManga(mangaId)
|
val mangaDetails = getManga(mangaId)
|
||||||
val source = getHttpSource(mangaDetails.sourceId)
|
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||||
|
|
||||||
val chapterList = source.fetchChapterList(
|
val chapterList = source.fetchChapterList(
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
@@ -62,7 +62,7 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
|||||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
return if (mangaEntry[MangaTable.initialized]) {
|
return if (mangaEntry[MangaTable.initialized]) {
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
@@ -34,10 +34,11 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
mangaEntry[MangaTable.description],
|
mangaEntry[MangaTable.description],
|
||||||
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])
|
||||||
)
|
)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
val fetchedManga = source.fetchMangaDetails(
|
val fetchedManga = source.fetchMangaDetails(
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
url = mangaEntry[MangaTable.url]
|
url = mangaEntry[MangaTable.url]
|
||||||
@@ -65,7 +66,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
@@ -78,7 +79,8 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
fetchedManga.description,
|
fetchedManga.description,
|
||||||
fetchedManga.genre,
|
fetchedManga.genre,
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
false
|
false,
|
||||||
|
getSource(mangaEntry[MangaTable.sourceReference])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +91,7 @@ fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
|||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
return getCachedResponse(saveDir, fileName) {
|
return getCachedResponse(saveDir, fileName) {
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
|||||||
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
sourceId,
|
sourceId.toString(),
|
||||||
|
|
||||||
manga.url,
|
manga.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
@@ -70,7 +70,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
|||||||
val mangaId = mangaEntry[MangaTable.id].value
|
val mangaId = mangaEntry[MangaTable.id].value
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
sourceId,
|
sourceId.toString(),
|
||||||
|
|
||||||
manga.url,
|
manga.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
|||||||
|
|
||||||
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
|
|||||||
|
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sourceGlobalSearch(searchTerm: String) {
|
fun sourceGlobalSearch(searchTerm: String) {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FilterWrapper(
|
data class FilterWrapper(
|
||||||
|
|||||||
@@ -15,14 +15,18 @@ import ir.armor.tachidesk.database.table.SourceTable
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.lang.NullPointerException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
||||||
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
||||||
|
|
||||||
fun getHttpSource(sourceId: Long): HttpSource {
|
fun getHttpSource(sourceId: Long): HttpSource {
|
||||||
|
val sourceRecord = transaction {
|
||||||
|
SourceEntity.findById(sourceId)
|
||||||
|
} ?: throw NullPointerException("Source with id $sourceId is not installed")
|
||||||
|
|
||||||
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
|
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
|
||||||
if (cachedResult != null) {
|
if (cachedResult != null) {
|
||||||
println("used cached HttpSource: ${cachedResult.second.name}")
|
println("used cached HttpSource: ${cachedResult.second.name}")
|
||||||
@@ -30,7 +34,6 @@ fun getHttpSource(sourceId: Long): HttpSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val result: HttpSource = transaction {
|
val result: HttpSource = transaction {
|
||||||
val sourceRecord = SourceEntity.findById(sourceId)!!
|
|
||||||
val extensionId = sourceRecord.extension.id.value
|
val extensionId = sourceRecord.extension.id.value
|
||||||
val extensionRecord = ExtensionEntity.findById(extensionId)!!
|
val extensionRecord = ExtensionEntity.findById(extensionId)!!
|
||||||
val apkName = extensionRecord.apkName
|
val apkName = extensionRecord.apkName
|
||||||
@@ -87,14 +90,14 @@ fun getSourceList(): List<SourceDataClass> {
|
|||||||
|
|
||||||
fun getSource(sourceId: Long): SourceDataClass {
|
fun getSource(sourceId: Long): SourceDataClass {
|
||||||
return transaction {
|
return transaction {
|
||||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
|
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||||
|
|
||||||
return@transaction SourceDataClass(
|
return@transaction SourceDataClass(
|
||||||
source[SourceTable.id].value.toString(),
|
sourceId.toString(),
|
||||||
source[SourceTable.name],
|
source?.get(SourceTable.name),
|
||||||
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
|
source?.get(SourceTable.lang),
|
||||||
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl],
|
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
||||||
getHttpSource(source[SourceTable.id].value).supportsLatest
|
source?.let { getHttpSource(sourceId).supportsLatest }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Kotlin source file was generated by the Gradle 'init' task.
|
|
||||||
*/
|
|
||||||
package ir.armor.tachidesk
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class AppTest {
|
|
||||||
@Test fun testAppHasAGreeting() {
|
|
||||||
assertTrue(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,13 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"@types/react-lazyload": "^3.1.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"fontsource-roboto": "^4.0.0",
|
"fontsource-roboto": "^4.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-lazyload": "^3.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.1",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4"
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ import useLocalStorage from './util/useLocalStorage';
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const [title, setTitle] = useState<string>('Tachidesk');
|
const [title, setTitle] = useState<string>('Tachidesk');
|
||||||
const [action, setAction] = useState<any>(<div />);
|
const [action, setAction] = useState<any>(<div />);
|
||||||
|
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
|
||||||
|
|
||||||
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
||||||
|
|
||||||
const navBarContext = {
|
const navBarContext = {
|
||||||
title, setTitle, action, setAction,
|
title, setTitle, action, setAction, override, setOverride,
|
||||||
};
|
};
|
||||||
const darkThemeContext = { darkTheme, setDarkTheme };
|
const darkThemeContext = { darkTheme, setDarkTheme };
|
||||||
|
|
||||||
@@ -63,7 +66,12 @@ export default function App() {
|
|||||||
<NavbarContext.Provider value={navBarContext}>
|
<NavbarContext.Provider value={navBarContext}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<Container maxWidth={false} disableGutters>
|
<Container
|
||||||
|
id="appMainContainer"
|
||||||
|
maxWidth={false}
|
||||||
|
disableGutters
|
||||||
|
style={{ paddingTop: '64px' }}
|
||||||
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/sources/:sourceId/search/">
|
<Route path="/sources/:sourceId/search/">
|
||||||
<Search />
|
<Search />
|
||||||
@@ -81,7 +89,7 @@ export default function App() {
|
|||||||
<Sources />
|
<Sources />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||||
<Reader />
|
<></>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/manga/:id">
|
<Route path="/manga/:id">
|
||||||
<Manga />
|
<Manga />
|
||||||
@@ -106,6 +114,11 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Container>
|
</Container>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||||
|
<Reader />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
</NavbarContext.Provider>
|
</NavbarContext.Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ export default function CategorySelect(props: IProps) {
|
|||||||
<DialogTitle>Set categories</DialogTitle>
|
<DialogTitle>Set categories</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
{categoryInfos.length === 0
|
||||||
|
&& (
|
||||||
|
<span>
|
||||||
|
No categories found!
|
||||||
|
<br />
|
||||||
|
You should make some from settings.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{categoryInfos.map((categoryInfo) => (
|
{categoryInfos.map((categoryInfo) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={(
|
control={(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Card from '@material-ui/core/Card';
|
|||||||
import CardContent from '@material-ui/core/CardContent';
|
import CardContent from '@material-ui/core/CardContent';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -41,6 +42,7 @@ interface IProps{
|
|||||||
|
|
||||||
export default function ChapterCard(props: IProps) {
|
export default function ChapterCard(props: IProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
const { chapter } = props;
|
const { chapter } = props;
|
||||||
|
|
||||||
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
|
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
|
||||||
@@ -64,7 +66,7 @@ export default function ChapterCard(props: IProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button>
|
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { history.push(`/manga/${chapter.mangaId}/chapter/${chapter.id}`); }}>open</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const useStyles = makeStyles({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
manga: IManga
|
manga: IMangaCard
|
||||||
}
|
}
|
||||||
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -2,17 +2,108 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { Button, createStyles, makeStyles } from '@material-ui/core';
|
import { makeStyles } from '@material-ui/core';
|
||||||
import React, { useState } from 'react';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import { Theme } from '@material-ui/core/styles';
|
||||||
|
import FavoriteIcon from '@material-ui/icons/Favorite';
|
||||||
|
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
|
||||||
|
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||||
|
import PublicIcon from '@material-ui/icons/Public';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
import CategorySelect from './CategorySelect';
|
import CategorySelect from './CategorySelect';
|
||||||
|
|
||||||
const useStyles = makeStyles(() => createStyles({
|
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
position: 'fixed',
|
||||||
|
width: '50vw',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
padding: '10px',
|
||||||
|
// [theme.breakpoints.up('md')]: {
|
||||||
|
// minWidth: '50%',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
leftRight: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row-reverse',
|
},
|
||||||
|
leftSide: {
|
||||||
|
'& img': {
|
||||||
|
borderRadius: 4,
|
||||||
|
maxWidth: '100%',
|
||||||
|
minWidth: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
maxWidth: '50%',
|
||||||
|
// [theme.breakpoints.up('md')]: {
|
||||||
|
// minWidth: '100px',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
rightSide: {
|
||||||
|
marginLeft: 15,
|
||||||
|
maxWidth: '100%',
|
||||||
|
'& span': {
|
||||||
|
fontWeight: '400',
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
fontSize: '1.3em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-around',
|
||||||
'& button': {
|
'& button': {
|
||||||
marginLeft: 10,
|
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
|
||||||
|
},
|
||||||
|
'& span': {
|
||||||
|
display: 'block',
|
||||||
|
fontSize: '0.85em',
|
||||||
|
},
|
||||||
|
'& a': {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: '#858585',
|
||||||
|
'& button': {
|
||||||
|
color: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
paddingLeft: '10px',
|
||||||
|
paddingRight: '10px',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
fontSize: '1.2em',
|
||||||
|
// maxWidth: '50%',
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
fontSize: '1.3em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
'& h4': {
|
||||||
|
marginTop: '1em',
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
'& p': {
|
||||||
|
textAlign: 'justify',
|
||||||
|
textJustify: 'inter-word',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
genre: {
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
'& h5': {
|
||||||
|
border: '2px solid #2196f3',
|
||||||
|
borderRadius: '1.13em',
|
||||||
|
marginRight: '1em',
|
||||||
|
marginTop: 0,
|
||||||
|
marginBottom: '10px',
|
||||||
|
padding: '0.3em',
|
||||||
|
color: '#2196f3',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -21,30 +112,70 @@ interface IProps{
|
|||||||
manga: IManga
|
manga: IManga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSourceName(source: ISource) {
|
||||||
|
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) {
|
export default function MangaDetails(props: IProps) {
|
||||||
const classes = useStyles();
|
const { setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
const { manga } = props;
|
const { manga } = props;
|
||||||
const [inLibrary, setInLibrary] = useState<string>(
|
const [inLibrary, setInLibrary] = useState<string>(
|
||||||
manga.inLibrary ? 'In Library' : 'Not In Library',
|
manga.inLibrary ? 'In Library' : 'Add to Library',
|
||||||
);
|
);
|
||||||
|
|
||||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inLibrary === 'In Library') {
|
||||||
|
setAction(
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setCategoryDialogOpen(true)}
|
||||||
|
aria-label="display more actions"
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<FilterListIcon />
|
||||||
|
</IconButton>
|
||||||
|
<CategorySelect
|
||||||
|
open={categoryDialogOpen}
|
||||||
|
setOpen={setCategoryDialogOpen}
|
||||||
|
mangaId={manga.id}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
|
||||||
|
);
|
||||||
|
} else { setAction(<></>); }
|
||||||
|
}, [inLibrary, categoryDialogOpen]);
|
||||||
|
|
||||||
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
|
const classes = useStyles(inLibrary)();
|
||||||
|
|
||||||
function addToLibrary() {
|
function addToLibrary() {
|
||||||
setInLibrary('adding');
|
// setInLibrary('adding');
|
||||||
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||||
setInLibrary('In Library');
|
setInLibrary('In Library');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromLibrary() {
|
function removeFromLibrary() {
|
||||||
setInLibrary('removing');
|
// setInLibrary('removing');
|
||||||
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||||
setInLibrary('Not In Library');
|
setInLibrary('Add To Library');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleButtonClick() {
|
function handleButtonClick() {
|
||||||
if (inLibrary === 'Not In Library') {
|
if (inLibrary === 'Add To Library') {
|
||||||
addToLibrary();
|
addToLibrary();
|
||||||
} else {
|
} else {
|
||||||
removeFromLibrary();
|
removeFromLibrary();
|
||||||
@@ -52,21 +183,64 @@ export default function MangaDetails(props: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classes.root}>
|
||||||
<h1>
|
<div className={classes.top}>
|
||||||
{manga && manga.title}
|
<div className={classes.leftRight}>
|
||||||
</h1>
|
<div className={classes.leftSide}>
|
||||||
<div className={classes.root}>
|
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
|
||||||
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
|
</div>
|
||||||
{inLibrary === 'In Library'
|
<div className={classes.rightSide}>
|
||||||
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
|
<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>
|
</div>
|
||||||
<CategorySelect
|
|
||||||
open={categoryDialogOpen}
|
|
||||||
setOpen={setCategoryDialogOpen}
|
|
||||||
mangaId={manga.id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Grid from '@material-ui/core/Grid';
|
|||||||
import MangaCard from './MangaCard';
|
import MangaCard from './MangaCard';
|
||||||
|
|
||||||
interface IProps{
|
interface IProps{
|
||||||
mangas: IManga[]
|
mangas: IMangaCard[]
|
||||||
message?: string
|
message?: string
|
||||||
hasNextPage: boolean
|
hasNextPage: boolean
|
||||||
lastPageNum: number
|
lastPageNum: number
|
||||||
@@ -48,7 +48,7 @@ export default function MangaGrid(props: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
|
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
|
||||||
{mapped}
|
{mapped}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
// TODO: remove above!
|
// TODO: remove above!
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
@@ -6,18 +5,14 @@
|
|||||||
|
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import MoreIcon from '@material-ui/icons/MoreVert';
|
|
||||||
import AppBar from '@material-ui/core/AppBar';
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
import Menu from '@material-ui/core/Menu';
|
|
||||||
|
|
||||||
import TemporaryDrawer from './TemporaryDrawer';
|
|
||||||
import NavBarContext from '../context/NavbarContext';
|
import NavBarContext from '../context/NavbarContext';
|
||||||
import DarkTheme from '../context/DarkTheme';
|
import DarkTheme from '../context/DarkTheme';
|
||||||
|
import TemporaryDrawer from './TemporaryDrawer';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -31,89 +26,40 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// const theme = createMuiTheme({
|
|
||||||
// overrides: {
|
|
||||||
// MuiAppBar: {
|
|
||||||
// colorPrimary: { backgroundColor: '#FFC0CB' },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// palette: { type: 'dark' },
|
|
||||||
// });
|
|
||||||
|
|
||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const { title, action, override } = useContext(NavBarContext);
|
||||||
const { title, action } = useContext(NavBarContext);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
|
|
||||||
const { darkTheme } = useContext(DarkTheme);
|
const { darkTheme } = useContext(DarkTheme);
|
||||||
|
|
||||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<>
|
||||||
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
|
{override.status && override.value}
|
||||||
<Toolbar>
|
{!override.status
|
||||||
<IconButton
|
&& (
|
||||||
edge="start"
|
<div className={classes.root}>
|
||||||
className={classes.menuButton}
|
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
|
||||||
color="inherit"
|
<Toolbar>
|
||||||
aria-label="menu"
|
<IconButton
|
||||||
disableRipple
|
edge="start"
|
||||||
onClick={() => setDrawerOpen(true)}
|
className={classes.menuButton}
|
||||||
>
|
color="inherit"
|
||||||
<MenuIcon />
|
aria-label="menu"
|
||||||
</IconButton>
|
disableRipple
|
||||||
<Typography variant="h6" className={classes.title}>
|
onClick={() => setDrawerOpen(true)}
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
{action}
|
|
||||||
{/* <IconButton
|
|
||||||
onClick={handleMenu}
|
|
||||||
aria-label="display more actions"
|
|
||||||
edge="end"
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<FilterListIcon />
|
|
||||||
</IconButton> */}
|
|
||||||
{/* <Menu
|
|
||||||
id="menu-appbar"
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => { setDarkTheme(true); handleClose(); }}
|
|
||||||
>
|
>
|
||||||
Dark Theme
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
</MenuItem>
|
<Typography variant="h6" className={classes.title}>
|
||||||
<MenuItem
|
{title}
|
||||||
onClick={() => { setDarkTheme(false); handleClose(); }}
|
</Typography>
|
||||||
>
|
{action}
|
||||||
Light Theme
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
</MenuItem>
|
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||||
</Menu> */}
|
</div>
|
||||||
</Toolbar>
|
)}
|
||||||
</AppBar>
|
</>
|
||||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
99
webUI/react/src/components/Page.tsx
Normal file
99
webUI/react/src/components/Page.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/* eslint-disable react/no-unused-prop-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import LazyLoad from 'react-lazyload';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
loading: {
|
||||||
|
margin: '100px auto',
|
||||||
|
height: '100vh',
|
||||||
|
},
|
||||||
|
loadingImage: {
|
||||||
|
padding: 'calc(50vh - 40px) calc(50vw - 40px)',
|
||||||
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
backgroundColor: '#525252',
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
src: string
|
||||||
|
index: number
|
||||||
|
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
}
|
||||||
|
|
||||||
|
function LazyImage(props: IProps) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { src, index, setCurPage } = props;
|
||||||
|
const [imageSrc, setImagsrc] = useState<string>('');
|
||||||
|
const ref = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (ref.current) {
|
||||||
|
const rect = ref.current.getBoundingClientRect();
|
||||||
|
if (rect.y < 0 && rect.y + rect.height > 0) {
|
||||||
|
setCurPage(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = src;
|
||||||
|
|
||||||
|
img.onload = () => setImagsrc(src);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (imageSrc.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={classes.loadingImage}>
|
||||||
|
<CircularProgress thickness={5} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={ref}
|
||||||
|
src={imageSrc}
|
||||||
|
alt={`Page #${index}`}
|
||||||
|
style={{ maxWidth: '100%' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page(props: IProps) {
|
||||||
|
const { src, index, setCurPage } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ margin: '0 auto' }}>
|
||||||
|
<LazyLoad
|
||||||
|
offset={window.innerHeight}
|
||||||
|
once
|
||||||
|
placeholder={(
|
||||||
|
<div className={classes.loading}>
|
||||||
|
<CircularProgress thickness={5} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LazyImage src={src} index={index} setCurPage={setCurPage} />
|
||||||
|
</LazyLoad>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
208
webUI/react/src/components/ReaderNavBar.tsx
Normal file
208
webUI/react/src/components/ReaderNavBar.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
|
||||||
|
import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
|
||||||
|
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Slide from '@material-ui/core/Slide';
|
||||||
|
import Fade from '@material-ui/core/Fade';
|
||||||
|
import Zoom from '@material-ui/core/Zoom';
|
||||||
|
import { Switch } from '@material-ui/core';
|
||||||
|
import NavBarContext from '../context/NavbarContext';
|
||||||
|
import DarkTheme from '../context/DarkTheme';
|
||||||
|
|
||||||
|
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
||||||
|
// main container and root div need to change classes...
|
||||||
|
AppMainContainer: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
AppRootElment: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
|
||||||
|
root: {
|
||||||
|
position: settings.staticNav ? 'sticky' : 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
minWidth: '300px',
|
||||||
|
height: '100vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
backgroundColor: '#0a0b0b',
|
||||||
|
|
||||||
|
'& header': {
|
||||||
|
backgroundColor: '#363b3d',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '64px',
|
||||||
|
paddingLeft: '24px',
|
||||||
|
paddingRight: '24px',
|
||||||
|
|
||||||
|
transition: 'left 2s ease',
|
||||||
|
|
||||||
|
'& button': {
|
||||||
|
flexGrow: 0,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
'& button:nth-child(1)': {
|
||||||
|
marginRight: '16px',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& button:nth-child(3)': {
|
||||||
|
marginRight: '-12px',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
openDrawerButton: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0 + 20,
|
||||||
|
left: 10 + 20,
|
||||||
|
height: '40px',
|
||||||
|
width: '40px',
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: 'black',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'black',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface IReaderSettings{
|
||||||
|
staticNav: boolean
|
||||||
|
showPageNumber: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultReaderSettings = () => ({
|
||||||
|
staticNav: false,
|
||||||
|
showPageNumber: true,
|
||||||
|
} as IReaderSettings);
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
settings: IReaderSettings
|
||||||
|
setSettings: React.Dispatch<React.SetStateAction<IReaderSettings>>
|
||||||
|
manga: IMangaCard | IManga
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReaderNavBar(props: IProps) {
|
||||||
|
const { title } = useContext(NavBarContext);
|
||||||
|
const { darkTheme } = useContext(DarkTheme);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { settings, setSettings, manga } = props;
|
||||||
|
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false || settings.staticNav);
|
||||||
|
const [hideOpenButton, setHideOpenButton] = useState(false);
|
||||||
|
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const classes = useStyles(settings)();
|
||||||
|
|
||||||
|
const setSettingValue = (key: string, value: any) => setSettings({ ...settings, [key]: value });
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const currentScrollPos = window.pageYOffset;
|
||||||
|
|
||||||
|
if (Math.abs(currentScrollPos - prevScrollPos) > 20) {
|
||||||
|
setHideOpenButton(currentScrollPos > prevScrollPos);
|
||||||
|
setPrevScrollPos(currentScrollPos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [handleScroll]);// handleScroll changes on every render
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
const rootEl = document.querySelector('#root')!;
|
||||||
|
const mainContainer = document.querySelector('#appMainContainer')!;
|
||||||
|
|
||||||
|
rootEl.classList.add(classes.AppRootElment);
|
||||||
|
mainContainer.classList.add(classes.AppMainContainer);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rootEl.classList.remove(classes.AppRootElment);
|
||||||
|
mainContainer.classList.remove(classes.AppMainContainer);
|
||||||
|
};
|
||||||
|
}, [handleScroll]);// handleScroll changes on every render
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Slide direction="right" in={drawerOpen} timeout={200} appear={false}>
|
||||||
|
<div className={classes.root}>
|
||||||
|
<header>
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
disableRipple
|
||||||
|
onClick={() => history.push(`/manga/${manga.id}`)}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h1">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{!settings.staticNav
|
||||||
|
&& (
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
disableRipple
|
||||||
|
onClick={() => setDrawerOpen(false)}
|
||||||
|
>
|
||||||
|
<KeyboardArrowLeftIcon />
|
||||||
|
</IconButton>
|
||||||
|
) }
|
||||||
|
</header>
|
||||||
|
<h3>Static Navigation</h3>
|
||||||
|
<Switch
|
||||||
|
checked={settings.staticNav}
|
||||||
|
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<h3>Show page number</h3>
|
||||||
|
<Switch
|
||||||
|
checked={settings.showPageNumber}
|
||||||
|
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Slide>
|
||||||
|
<Zoom in={!drawerOpen}>
|
||||||
|
<Fade in={!hideOpenButton}>
|
||||||
|
<IconButton
|
||||||
|
className={classes.openDrawerButton}
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
disableRipple
|
||||||
|
disableFocusRipple
|
||||||
|
onClick={() => setDrawerOpen(true)}
|
||||||
|
>
|
||||||
|
<KeyboardArrowRightIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Fade>
|
||||||
|
</Zoom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,68 +27,54 @@ interface IProps {
|
|||||||
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const sideList = (side: 'left') => (
|
|
||||||
<div
|
|
||||||
className={classes.list}
|
|
||||||
role="presentation"
|
|
||||||
onClick={() => setDrawerOpen(false)}
|
|
||||||
onKeyDown={() => setDrawerOpen(false)}
|
|
||||||
>
|
|
||||||
<List>
|
|
||||||
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
<ListItem button key="Library">
|
|
||||||
<ListItemIcon>
|
|
||||||
<InboxIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Library" />
|
|
||||||
</ListItem>
|
|
||||||
</Link>
|
|
||||||
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
<ListItem button key="Extensions">
|
|
||||||
<ListItemIcon>
|
|
||||||
<InboxIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Extensions" />
|
|
||||||
</ListItem>
|
|
||||||
</Link>
|
|
||||||
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
<ListItem button key="Sources">
|
|
||||||
<ListItemIcon>
|
|
||||||
<InboxIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Sources" />
|
|
||||||
</ListItem>
|
|
||||||
</Link>
|
|
||||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
<ListItem button key="settings">
|
|
||||||
<ListItemIcon>
|
|
||||||
<InboxIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Settings" />
|
|
||||||
</ListItem>
|
|
||||||
</Link>
|
|
||||||
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
<ListItem button key="Search">
|
|
||||||
<ListItemIcon>
|
|
||||||
<InboxIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Global Search" />
|
|
||||||
</ListItem>
|
|
||||||
</Link> */}
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Drawer
|
<Drawer
|
||||||
BackdropProps={{ invisible: true }}
|
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
anchor="left"
|
anchor="left"
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
>
|
>
|
||||||
{sideList('left')}
|
<div
|
||||||
|
className={classes.list}
|
||||||
|
role="presentation"
|
||||||
|
onClick={() => setDrawerOpen(false)}
|
||||||
|
onKeyDown={() => setDrawerOpen(false)}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="Library">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Library" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="Extensions">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Extensions" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="Sources">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Sources" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="settings">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Settings" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ type ContextType = {
|
|||||||
setTitle: React.Dispatch<React.SetStateAction<string>>
|
setTitle: React.Dispatch<React.SetStateAction<string>>
|
||||||
action: any
|
action: any
|
||||||
setAction: React.Dispatch<React.SetStateAction<any>>
|
setAction: React.Dispatch<React.SetStateAction<any>>
|
||||||
|
override: INavbarOverride
|
||||||
|
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavBarContext = React.createContext<ContextType>({
|
const NavBarContext = React.createContext<ContextType>({
|
||||||
@@ -16,6 +18,8 @@ const NavBarContext = React.createContext<ContextType>({
|
|||||||
setTitle: ():void => {},
|
setTitle: ():void => {},
|
||||||
action: <div />,
|
action: <div />,
|
||||||
setAction: ():void => {},
|
setAction: ():void => {},
|
||||||
|
override: { status: false, value: <div /> },
|
||||||
|
setOverride: ():void => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default NavBarContext;
|
export default NavBarContext;
|
||||||
|
|||||||
@@ -35,9 +35,13 @@ function groupExtensions(extensions: IExtension[]) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extensionDefaultLangs() {
|
||||||
|
return [...defualtLangs(), 'all'];
|
||||||
|
}
|
||||||
|
|
||||||
export default function Extensions() {
|
export default function Extensions() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle('Extensions');
|
setTitle('Extensions');
|
||||||
@@ -76,7 +80,7 @@ export default function Extensions() {
|
|||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
Object.entries(extensions).map(([lang, list]) => (
|
Object.entries(extensions).map(([lang, list]) => (
|
||||||
(['installed', ...shownLangs].indexOf(lang) !== -1
|
((['installed', ...shownLangs].indexOf(lang) !== -1 && (list as []).length > 0)
|
||||||
&& (
|
&& (
|
||||||
<React.Fragment key={lang}>
|
<React.Fragment key={lang}>
|
||||||
<h1 key={lang} style={{ marginLeft: 25 }}>
|
<h1 key={lang} style={{ marginLeft: 25 }}>
|
||||||
|
|||||||
@@ -1,17 +1,45 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
|
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
import ChapterCard from '../components/ChapterCard';
|
import ChapterCard from '../components/ChapterCard';
|
||||||
import MangaDetails from '../components/MangaDetails';
|
import MangaDetails from '../components/MangaDetails';
|
||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
root: {
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
chapters: {
|
||||||
|
listStyle: 'none',
|
||||||
|
padding: 0,
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: '50%',
|
||||||
|
marginLeft: '50%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
loading: {
|
||||||
|
margin: '10px 0',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export default function Manga() {
|
export default function Manga() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const classes = useStyles();
|
||||||
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
|
|
||||||
|
const { setTitle } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
|
||||||
|
|
||||||
const { id } = useParams<{id: string}>();
|
const { id } = useParams<{id: string}>();
|
||||||
|
|
||||||
@@ -33,16 +61,22 @@ export default function Manga() {
|
|||||||
.then((data) => setChapters(data));
|
.then((data) => setChapters(data));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const chapterCards = chapters.map((chapter) => (
|
const chapterCards = (
|
||||||
<ol style={{ listStyle: 'none', padding: 0 }}>
|
<ol className={classes.chapters}>
|
||||||
<ChapterCard chapter={chapter} />
|
{chapters.length === 0
|
||||||
|
&& (
|
||||||
|
<div className={classes.loading}>
|
||||||
|
<CircularProgress thickness={5} />
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
|
||||||
</ol>
|
</ol>
|
||||||
));
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={classes.root}>
|
||||||
{manga && <MangaDetails manga={manga} />}
|
{manga && <MangaDetails manga={manga} />}
|
||||||
{chapterCards}
|
{chapterCards}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,111 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import Page from '../components/Page';
|
||||||
|
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
|
||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
import useLocalStorage from '../util/useLocalStorage';
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
|
||||||
const style = {
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
display: 'flex',
|
reader: {
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
flexDirection: 'column',
|
||||||
margin: '0 auto',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#343a40',
|
margin: '0 auto',
|
||||||
} as React.CSSProperties;
|
},
|
||||||
|
|
||||||
|
loading: {
|
||||||
|
margin: '50px auto',
|
||||||
|
},
|
||||||
|
|
||||||
|
pageNumber: {
|
||||||
|
display: settings.showPageNumber ? 'block' : 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '50px',
|
||||||
|
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
||||||
|
width: '50px',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
|
|
||||||
export default function Reader() {
|
export default function Reader() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
|
||||||
useEffect(() => { setTitle('Reader'); setAction(<></>); }, []);
|
|
||||||
|
const classes = useStyles(settings)();
|
||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
const [pageCount, setPageCount] = useState<number>(-1);
|
|
||||||
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
||||||
|
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
||||||
|
const [pageCount, setPageCount] = useState<number>(-1);
|
||||||
|
const [curPage, setCurPage] = useState<number>(0);
|
||||||
|
|
||||||
|
const { setOverride, setTitle } = useContext(NavbarContext);
|
||||||
|
useEffect(() => {
|
||||||
|
setOverride(
|
||||||
|
{
|
||||||
|
status: true,
|
||||||
|
value: <ReaderNavBar
|
||||||
|
settings={settings}
|
||||||
|
setSettings={setSettings}
|
||||||
|
manga={manga}
|
||||||
|
/>,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// clean up for when we leave the reader
|
||||||
|
return () => setOverride({ status: false, value: <div /> });
|
||||||
|
}, [manga, settings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle('Reader');
|
||||||
|
client.get(`/api/v1/manga/${mangaId}/`)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data: IManga) => {
|
||||||
|
setManga(data);
|
||||||
|
setTitle(data.title);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data:IChapter) => {
|
||||||
setTitle(data.name);
|
|
||||||
setPageCount(data.pageCount);
|
setPageCount(data.pageCount);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (pageCount === -1) {
|
if (pageCount === -1) {
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div className={classes.loading}>
|
||||||
<h3>wait</h3>
|
<CircularProgress thickness={5} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapped = range(pageCount).map((index) => (
|
|
||||||
<div style={{ margin: '0 auto' }}>
|
|
||||||
<img src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="F" style={{ maxWidth: '100%' }} />
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div className={classes.reader}>
|
||||||
{mapped}
|
<div className={classes.pageNumber}>
|
||||||
|
{`${curPage + 1} / ${pageCount}`}
|
||||||
|
</div>
|
||||||
|
{range(pageCount).map((index) => (
|
||||||
|
<Page
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function SearchSingle() {
|
|||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||||
const [message, setMessage] = useState<string>('');
|
const [message, setMessage] = useState<string>('');
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||||
|
|
||||||
|
|||||||
26
webUI/react/src/typings.d.ts
vendored
26
webUI/react/src/typings.d.ts
vendored
@@ -21,11 +21,28 @@ interface ISource {
|
|||||||
history: any
|
history: any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IManga {
|
interface IMangaCard {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
inLibrary?: boolean
|
}
|
||||||
|
|
||||||
|
interface IManga {
|
||||||
|
id: number
|
||||||
|
sourceId: string
|
||||||
|
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
thumbnailUrl: string
|
||||||
|
|
||||||
|
artist: string
|
||||||
|
author: string
|
||||||
|
description: string
|
||||||
|
genre: string
|
||||||
|
status: string
|
||||||
|
|
||||||
|
inLibrary: boolean
|
||||||
|
source: ISource
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IChapter {
|
interface IChapter {
|
||||||
@@ -45,3 +62,8 @@ interface ICategory {
|
|||||||
name: String
|
name: String
|
||||||
isLanding: boolean
|
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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { useState, Dispatch, SetStateAction } from 'react';
|
import React, { useState, Dispatch, SetStateAction } from 'react';
|
||||||
import storage from './localStorage';
|
import storage from './localStorage';
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
export default function useLocalStorage<T>(key: string, defaultValue: T) : [T, Dispatch<SetStateAction<T>>] {
|
export default function useLocalStorage<T>(key: string, defaultValue: T | (() => T)) : [T, Dispatch<SetStateAction<T>>] {
|
||||||
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, defaultValue));
|
const initialState = defaultValue instanceof Function ? defaultValue() : defaultValue;
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, initialState));
|
||||||
|
|
||||||
const setValue = (value: T | ((prevState: T) => T)) => {
|
const setValue = ((value: T | ((prevState: T) => T)) => {
|
||||||
// Allow value to be a function so we have same API as useState
|
// Allow value to be a function so we have same API as useState
|
||||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||||
setStoredValue(valueToStore);
|
setStoredValue(valueToStore);
|
||||||
storage.setItem(key, valueToStore);
|
storage.setItem(key, valueToStore);
|
||||||
};
|
}) as React.Dispatch<React.SetStateAction<T>>;
|
||||||
|
|
||||||
return [storedValue, setValue];
|
return [storedValue, setValue];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1846,6 +1846,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-lazyload@^3.1.0":
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-lazyload/-/react-lazyload-3.1.0.tgz#97b167266afbc75f432eca01c50555adae21c5a4"
|
||||||
|
integrity sha512-JnVJb+6cUrIk4Fo/zc/4NuFtm0h3XeNlN4Gt++WEHGeUDtlhnF1lXRz0WoqNmh5gH3oyeYOJXIZ8MoPL9ehp0g==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-router-dom@^5.1.6":
|
"@types/react-router-dom@^5.1.6":
|
||||||
version "5.1.6"
|
version "5.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb"
|
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb"
|
||||||
@@ -9353,6 +9360,11 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||||
|
|
||||||
|
react-lazyload@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-3.2.0.tgz#497bd06a6dbd7015e3376e1137a67dc47d2dd021"
|
||||||
|
integrity sha512-zJlrG8QyVZz4+xkYZH5v1w3YaP5wEFaYSUWC4CT9UXfK75IfRAIEdnyIUF+dXr3kX2MOtL1lUaZmaQZqrETwgw==
|
||||||
|
|
||||||
react-redux@^7.1.1:
|
react-redux@^7.1.1:
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
|
||||||
|
|||||||
Reference in New Issue
Block a user