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 |
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
+16
-3
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Vendored
+24
-2
@@ -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