Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 083996a48d | |||
| 9d38f478e3 | |||
| 57274a0a01 | |||
| b3b56b7fc8 | |||
| 0b690577da | |||
| e9683a3a37 | |||
| f8f67b3eba | |||
| 7b16b082d8 | |||
| 2a783f0d8e | |||
| 42ae32de33 | |||
| cec7ddc486 | |||
| 9c55fc3868 | |||
| 104c5a8d83 | |||
| 7450b16742 | |||
| 3ecd0931a1 | |||
| 2f2a52ae2f | |||
| f464087c30 | |||
| 2364960388 | |||
| 76be4d64cd | |||
| 7d98e8ce47 | |||
| 40831fc681 | |||
| e38e7ccf26 | |||
| 98b9e2f2cf | |||
| 4bf3c12f76 | |||
| bab25f9ad9 | |||
| a62ee8f8c3 | |||
| 5f23691e20 | |||
| 3de9ccc62f | |||
| 1896f7f37b | |||
| 490643dc02 | |||
| 9808976088 |
@@ -10,6 +10,7 @@ cp -f $new_jar_build Tachidesk-latest.jar
|
|||||||
|
|
||||||
rm -rf latest_pointer/*
|
rm -rf latest_pointer/*
|
||||||
cp $new_jar_build latest_pointer
|
cp $new_jar_build latest_pointer
|
||||||
|
cp ../master/server/build/Tachidesk-*.zip latest_pointer
|
||||||
|
|
||||||
latest=$(ls *.jar | tail -n1 | sed -e's/Tachidesk-\|.jar//g')
|
latest=$(ls *.jar | tail -n1 | sed -e's/Tachidesk-\|.jar//g')
|
||||||
echo "{ \"latest\": \"$latest\" }" > index.json
|
echo "{ \"latest\": \"$latest\" }" > index.json
|
||||||
|
|||||||
@@ -59,16 +59,21 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar and launch4j
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
|
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||||
wrapper-cache-enabled: true
|
wrapper-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: make windows package
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows-bundler.sh
|
||||||
|
|
||||||
- name: Checkout preview branch
|
- name: Checkout preview branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -56,54 +56,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar and launch4j
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
|
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||||
wrapper-cache-enabled: true
|
wrapper-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: make windows package
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows-bundler.sh
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@master
|
uses: xresloader/upload-to-github-release@master
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
file: "master/server/build/*.jar;master/server/build/*-win32.zip"
|
file: "master/server/build/*.jar;master/server/build/*.zip"
|
||||||
tags: true
|
tags: true
|
||||||
draft: true
|
draft: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
# - name: Create Release
|
|
||||||
# id: create_release
|
|
||||||
# uses: actions/create-release@v1
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# tag_name: ${{ github.ref }}
|
|
||||||
# release_name: Release ${{ github.ref }}
|
|
||||||
# body: |
|
|
||||||
# Release body
|
|
||||||
# draft: false
|
|
||||||
# prerelease: true
|
|
||||||
#
|
|
||||||
# - name: Get the Ref
|
|
||||||
# id: get-ref
|
|
||||||
# uses: ankitvgupta/ref-to-tag-action@master
|
|
||||||
# with:
|
|
||||||
# ref: ${{ github.ref }}
|
|
||||||
# head_ref: ${{ github.head_ref }}
|
|
||||||
#
|
|
||||||
# - name: Get the tag
|
|
||||||
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
|
|
||||||
#
|
|
||||||
# - name: Upload Release
|
|
||||||
# uses: AButler/upload-release-assets@v2.0
|
|
||||||
# with:
|
|
||||||
# files: 'master/repo/*'
|
|
||||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# release-tag: ${{ steps.get-ref.outputs.tag }}
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Here is a list of current features:
|
|||||||
- Ability to download Mangas for offline read(This partially works)
|
- Ability to download Mangas for offline read(This partially works)
|
||||||
- Backup and restore support powered by Tachiyomi Legacy Backups
|
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||||
|
|
||||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
|
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
|
||||||
|
|
||||||
## Downloading and Running the app
|
## Downloading and Running the app
|
||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
echo "Downloading packr jar..."
|
||||||
|
|
||||||
|
packr="packr-all-4.0.0.jar"
|
||||||
|
curl -L "https://github.com/libgdx/packr/releases/download/4.0.0/packr-all-4.0.0.jar" -o $packr
|
||||||
|
|
||||||
|
echo "Downloading jre..."
|
||||||
|
|
||||||
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
|
||||||
|
|
||||||
|
echo "creating windows bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
|
||||||
|
|
||||||
|
cp $jar "Tachidesk.jar"
|
||||||
|
|
||||||
|
java -jar $packr \
|
||||||
|
--platform windows64 \
|
||||||
|
--jdk $jre \
|
||||||
|
--executable Tachidesk \
|
||||||
|
--classpath Tachidesk.jar \
|
||||||
|
--mainclass ir.armor.tachidesk.MainKt \
|
||||||
|
--vmargs Xmx4G \
|
||||||
|
--output $release_name
|
||||||
|
|
||||||
|
cp resources/Tachidesk-debug.bat $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
cp $zip_name ../server/build/
|
||||||
+3
-53
@@ -83,7 +83,7 @@ dependencies {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val MainClass = "ir.armor.tachidesk.Main"
|
val MainClass = "ir.armor.tachidesk.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.3.1"
|
val tachideskVersion = "v0.3.3"
|
||||||
|
|
||||||
// counts commit count on master
|
// counts commit count on master
|
||||||
val tachideskRevision = Runtime
|
val tachideskRevision = Runtime
|
||||||
@@ -131,7 +131,7 @@ launch4j { //used for windows
|
|||||||
bundledJrePath = "jre"
|
bundledJrePath = "jre"
|
||||||
bundledJre64Bit = true
|
bundledJre64Bit = true
|
||||||
jreMinVersion = "8"
|
jreMinVersion = "8"
|
||||||
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"
|
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win64"
|
||||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
||||||
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
|
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
|
||||||
}
|
}
|
||||||
@@ -169,56 +169,6 @@ tasks {
|
|||||||
useJUnit()
|
useJUnit()
|
||||||
}
|
}
|
||||||
|
|
||||||
register<Zip>("windowsPackage") {
|
|
||||||
from(fileTree("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"))
|
|
||||||
destinationDirectory.set(File("$buildDir"))
|
|
||||||
archiveFileName.set("${rootProject.name}-$tachideskVersion-$tachideskRevision-win32.zip")
|
|
||||||
dependsOn("windowsPackageWorkaround2")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Delete>("windowsPackageWorkaround2") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jre",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/lib",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/server.exe",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32"
|
|
||||||
)
|
|
||||||
dependsOn("windowsPackageWorkaround")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Copy>("windowsPackageWorkaround") {
|
|
||||||
from("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
dependsOn("deleteUnwantedJreDir")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Delete>("deleteUnwantedJreDir") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jdk8u282-b08-jre"
|
|
||||||
)
|
|
||||||
dependsOn("addJreToDistributable")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Copy>("addJreToDistributable") {
|
|
||||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
|
||||||
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
eachFile {
|
|
||||||
path = path.replace(".*-jre".toRegex(), "jre")
|
|
||||||
}
|
|
||||||
dependsOn("downloadJre")
|
|
||||||
dependsOn("createExe")
|
|
||||||
}
|
|
||||||
|
|
||||||
named("createExe") {
|
|
||||||
dependsOn("shadowJar")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
|
||||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
|
||||||
dest("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
|
||||||
overwrite(false)
|
|
||||||
onlyIfModified(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<ShadowJar> {
|
withType<ShadowJar> {
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
|
|||||||
@@ -10,13 +10,7 @@ package ir.armor.tachidesk
|
|||||||
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
||||||
import ir.armor.tachidesk.server.applicationSetup
|
import ir.armor.tachidesk.server.applicationSetup
|
||||||
|
|
||||||
class Main {
|
fun main() {
|
||||||
companion object {
|
applicationSetup()
|
||||||
|
javalinSetup()
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
applicationSetup()
|
|
||||||
javalinSetup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ object CachedImageResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||||
|
val target = "$fileName."
|
||||||
File(directoryPath).listFiles().forEach { file ->
|
File(directoryPath).listFiles().forEach { file ->
|
||||||
if (file.name.startsWith("$fileName."))
|
if (file.name.startsWith(target))
|
||||||
return "$directoryPath/${file.name}"
|
return "$directoryPath/${file.name}"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ import org.kodein.di.DI
|
|||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
object DBMangaer {
|
object DBManager {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
@@ -24,7 +24,7 @@ object DBMangaer {
|
|||||||
|
|
||||||
fun databaseUp() {
|
fun databaseUp() {
|
||||||
// must mention db object so the lazy block executes
|
// must mention db object so the lazy block executes
|
||||||
val db = DBMangaer.db
|
val db = DBManager.db
|
||||||
db.useNestedTransactions = true
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
||||||
+87
-70
@@ -14,104 +14,121 @@ import org.jetbrains.exposed.dao.id.IntIdTable
|
|||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
class M0001_Initial : Migration() {
|
class M0001_Initial : Migration() {
|
||||||
private object ExtensionTable : IntIdTable() {
|
private class ExtensionTable : IntIdTable() {
|
||||||
val apkName = varchar("apk_name", 1024)
|
init {
|
||||||
|
varchar("apk_name", 1024)
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
varchar("name", 128)
|
||||||
|
varchar("pkg_name", 128)
|
||||||
|
varchar("version_name", 16)
|
||||||
|
integer("version_code")
|
||||||
|
varchar("lang", 10)
|
||||||
|
bool("is_nsfw")
|
||||||
|
|
||||||
// default is the local source icon from tachiyomi
|
bool("is_installed").default(false)
|
||||||
val iconUrl = varchar("icon_url", 2048)
|
bool("has_update").default(false)
|
||||||
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
bool("is_obsolete").default(false)
|
||||||
|
|
||||||
val name = varchar("name", 128)
|
varchar("class_name", 1024).default("") // fully qualified name
|
||||||
val pkgName = varchar("pkg_name", 128)
|
}
|
||||||
val versionName = varchar("version_name", 16)
|
|
||||||
val versionCode = integer("version_code")
|
|
||||||
val lang = varchar("lang", 10)
|
|
||||||
val isNsfw = bool("is_nsfw")
|
|
||||||
|
|
||||||
val isInstalled = bool("is_installed").default(false)
|
|
||||||
val hasUpdate = bool("has_update").default(false)
|
|
||||||
val isObsolete = bool("is_obsolete").default(false)
|
|
||||||
|
|
||||||
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object SourceTable : IdTable<Long>() {
|
private class SourceTable(extensionTable: ExtensionTable) : IdTable<Long>() {
|
||||||
override val id = long("id").entityId()
|
override val id = long("id").entityId()
|
||||||
val name = varchar("name", 128)
|
init {
|
||||||
val lang = varchar("lang", 10)
|
varchar("name", 128)
|
||||||
val extension = reference("extension", ExtensionTable)
|
varchar("lang", 10)
|
||||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
reference("extension", extensionTable)
|
||||||
|
bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object MangaTable : IntIdTable() {
|
private class MangaTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
init {
|
||||||
val title = varchar("title", 512)
|
varchar("url", 2048)
|
||||||
val initialized = bool("initialized").default(false)
|
varchar("title", 512)
|
||||||
|
bool("initialized").default(false)
|
||||||
|
|
||||||
val artist = varchar("artist", 64).nullable()
|
varchar("artist", 64).nullable()
|
||||||
val author = varchar("author", 64).nullable()
|
varchar("author", 64).nullable()
|
||||||
val description = varchar("description", 4096).nullable()
|
varchar("description", 4096).nullable()
|
||||||
val genre = varchar("genre", 1024).nullable()
|
varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
val status = integer("status").default(SManga.UNKNOWN)
|
integer("status").default(SManga.UNKNOWN)
|
||||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
val inLibrary = bool("in_library").default(false)
|
bool("in_library").default(false)
|
||||||
val defaultCategory = bool("default_category").default(true)
|
bool("default_category").default(true)
|
||||||
|
|
||||||
// source is used by some ancestor of IntIdTable
|
// source is used by some ancestor of IntIdTable
|
||||||
val sourceReference = long("source")
|
long("source")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ChapterTable : IntIdTable() {
|
private class ChapterTable(mangaTable: MangaTable) : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
init {
|
||||||
val name = varchar("name", 512)
|
varchar("url", 2048)
|
||||||
val date_upload = long("date_upload").default(0)
|
varchar("name", 512)
|
||||||
val chapter_number = float("chapter_number").default(-1f)
|
long("date_upload").default(0)
|
||||||
val scanlator = varchar("scanlator", 128).nullable()
|
float("chapter_number").default(-1f)
|
||||||
|
varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
val isRead = bool("read").default(false)
|
bool("read").default(false)
|
||||||
val isBookmarked = bool("bookmark").default(false)
|
bool("bookmark").default(false)
|
||||||
val lastPageRead = integer("last_page_read").default(0)
|
integer("last_page_read").default(0)
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
integer("number_in_list")
|
||||||
|
reference("manga", mangaTable)
|
||||||
val manga = reference("manga", MangaTable)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object PageTable : IntIdTable() {
|
private class PageTable(chapterTable: ChapterTable) : IntIdTable() {
|
||||||
val index = integer("index")
|
init {
|
||||||
val url = varchar("url", 2048)
|
integer("index")
|
||||||
val imageUrl = varchar("imageUrl", 2048).nullable()
|
varchar("url", 2048)
|
||||||
|
varchar("imageUrl", 2048).nullable()
|
||||||
val chapter = reference("chapter", ChapterTable)
|
reference("chapter", chapterTable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CategoryTable : IntIdTable() {
|
private class CategoryTable : IntIdTable() {
|
||||||
val name = varchar("name", 64)
|
init {
|
||||||
val isLanding = bool("is_landing").default(false)
|
varchar("name", 64)
|
||||||
val order = integer("order").default(0)
|
bool("is_landing").default(false)
|
||||||
|
integer("order").default(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CategoryMangaTable : IntIdTable() {
|
private class CategoryMangaTable : IntIdTable() {
|
||||||
val category = reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
init {
|
||||||
val manga = reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
||||||
|
reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** initial migration, create all tables */
|
/** initial migration, create all tables */
|
||||||
override fun run() {
|
override fun run() {
|
||||||
transaction {
|
transaction {
|
||||||
|
val extensionTable = ExtensionTable()
|
||||||
|
val sourceTable = SourceTable(extensionTable)
|
||||||
|
val mangaTable = MangaTable()
|
||||||
|
val chapterTable = ChapterTable(mangaTable)
|
||||||
|
val pageTable = PageTable(chapterTable)
|
||||||
|
val categoryTable = CategoryTable()
|
||||||
|
val categoryMangaTable = CategoryMangaTable()
|
||||||
SchemaUtils.create(
|
SchemaUtils.create(
|
||||||
ExtensionTable,
|
extensionTable,
|
||||||
ExtensionTable,
|
sourceTable,
|
||||||
SourceTable,
|
mangaTable,
|
||||||
MangaTable,
|
chapterTable,
|
||||||
ChapterTable,
|
pageTable,
|
||||||
PageTable,
|
categoryTable,
|
||||||
CategoryTable,
|
categoryMangaTable,
|
||||||
CategoryMangaTable,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -11,6 +11,7 @@ import ir.armor.tachidesk.model.database.migration.lib.Migration
|
|||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
class M0002_ChapterTableIndexRename : Migration() {
|
class M0002_ChapterTableIndexRename : Migration() {
|
||||||
/** this migration renamed ChapterTable.NUMBER_IN_LIST to ChapterTable.INDEX */
|
/** this migration renamed ChapterTable.NUMBER_IN_LIST to ChapterTable.INDEX */
|
||||||
override fun run() {
|
override fun run() {
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
|||||||
logger.info { "Migrations finished successfully" }
|
logger.info { "Migrations finished successfully" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
||||||
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
||||||
.getTopLevelClasses(classPath)
|
.getTopLevelClasses(classPath)
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ object CategoryTable : IntIdTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||||
categoryEntry[CategoryTable.id].value,
|
categoryEntry[this.id].value,
|
||||||
categoryEntry[CategoryTable.order],
|
categoryEntry[this.order],
|
||||||
categoryEntry[CategoryTable.name],
|
categoryEntry[this.name],
|
||||||
categoryEntry[CategoryTable.isLanding],
|
categoryEntry[this.isLanding],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ object ChapterTable : IntIdTable() {
|
|||||||
|
|
||||||
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||||
ChapterDataClass(
|
ChapterDataClass(
|
||||||
chapterEntry[ChapterTable.url],
|
chapterEntry[this.url],
|
||||||
chapterEntry[ChapterTable.name],
|
chapterEntry[this.name],
|
||||||
chapterEntry[ChapterTable.date_upload],
|
chapterEntry[this.date_upload],
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
chapterEntry[this.chapter_number],
|
||||||
chapterEntry[ChapterTable.scanlator],
|
chapterEntry[this.scanlator],
|
||||||
chapterEntry[ChapterTable.manga].value,
|
chapterEntry[this.manga].value,
|
||||||
chapterEntry[ChapterTable.isRead],
|
chapterEntry[this.isRead],
|
||||||
chapterEntry[ChapterTable.isBookmarked],
|
chapterEntry[this.isBookmarked],
|
||||||
chapterEntry[ChapterTable.lastPageRead],
|
chapterEntry[this.lastPageRead],
|
||||||
chapterEntry[ChapterTable.chapterIndex],
|
chapterEntry[this.chapterIndex],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,21 +36,21 @@ object MangaTable : IntIdTable() {
|
|||||||
|
|
||||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaEntry[MangaTable.id].value,
|
mangaEntry[this.id].value,
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
mangaEntry[this.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[this.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[this.title],
|
||||||
proxyThumbnailUrl(mangaEntry[MangaTable.id].value),
|
proxyThumbnailUrl(mangaEntry[this.id].value),
|
||||||
|
|
||||||
mangaEntry[MangaTable.initialized],
|
mangaEntry[this.initialized],
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
mangaEntry[this.artist],
|
||||||
mangaEntry[MangaTable.author],
|
mangaEntry[this.author],
|
||||||
mangaEntry[MangaTable.description],
|
mangaEntry[this.description],
|
||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[this.genre],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
MangaStatus.valueOf(mangaEntry[this.status]).name,
|
||||||
mangaEntry[MangaTable.inLibrary]
|
mangaEntry[this.inLibrary]
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class MangaStatus(val status: Int) {
|
enum class MangaStatus(val status: Int) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ir.armor.tachidesk.server
|
package ir.armor.tachidesk.server
|
||||||
|
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.impl.Category.createCategory
|
import ir.armor.tachidesk.impl.Category.createCategory
|
||||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||||
import ir.armor.tachidesk.impl.Category.removeCategory
|
import ir.armor.tachidesk.impl.Category.removeCategory
|
||||||
@@ -37,14 +36,14 @@ import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBac
|
|||||||
import ir.armor.tachidesk.server.internal.About.getAbout
|
import ir.armor.tachidesk.server.internal.About.getAbout
|
||||||
import ir.armor.tachidesk.server.util.openInBrowser
|
import ir.armor.tachidesk.server.util.openInBrowser
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.future.future
|
import kotlinx.coroutines.future.future
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -56,7 +55,8 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
object JavalinSetup {
|
object JavalinSetup {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
||||||
|
private val scope = CoroutineScope(Executors.newFixedThreadPool(200).asCoroutineDispatcher())
|
||||||
|
|
||||||
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
||||||
return scope.future(block = block)
|
return scope.future(block = block)
|
||||||
@@ -68,7 +68,7 @@ object JavalinSetup {
|
|||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
try {
|
try {
|
||||||
// if the bellow line throws an exception then webUI is not bundled
|
// if the bellow line throws an exception then webUI is not bundled
|
||||||
Main::class.java.getResource("/react/index.html")
|
this::class.java.getResource("/react/index.html")
|
||||||
|
|
||||||
// no exception so we can tell javalin to serve webUI
|
// no exception so we can tell javalin to serve webUI
|
||||||
hasWebUiBundled = true
|
hasWebUiBundled = true
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
|||||||
val socksProxyPort: String by config
|
val socksProxyPort: String by config
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
val debugLogsEnabled: Boolean by config
|
val debugLogsEnabled: Boolean = System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("debugLogsEnabled")).toBoolean()
|
||||||
val systemTrayEnabled: Boolean by config
|
val systemTrayEnabled: Boolean by config
|
||||||
val initialOpenInBrowserEnabled: Boolean by config
|
val initialOpenInBrowserEnabled: Boolean by config
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package ir.armor.tachidesk.server
|
|||||||
|
|
||||||
import ch.qos.logback.classic.Level
|
import ch.qos.logback.classic.Level
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.model.database.databaseUp
|
import ir.armor.tachidesk.model.database.databaseUp
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
import ir.armor.tachidesk.server.util.systemTray
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
@@ -81,7 +80,7 @@ fun applicationSetup() {
|
|||||||
try {
|
try {
|
||||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||||
if (!dataConfFile.exists()) {
|
if (!dataConfFile.exists()) {
|
||||||
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
JavalinSetup::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
||||||
dataConfFile.outputStream().use { output ->
|
dataConfFile.outputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
@@ -97,7 +96,7 @@ fun applicationSetup() {
|
|||||||
if (serverConfig.systemTrayEnabled) {
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTray
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ import dorkbox.systemTray.SystemTray
|
|||||||
import dorkbox.systemTray.SystemTray.TrayType
|
import dorkbox.systemTray.SystemTray.TrayType
|
||||||
import dorkbox.util.CacheUtil
|
import dorkbox.util.CacheUtil
|
||||||
import dorkbox.util.Desktop
|
import dorkbox.util.Desktop
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.server.BuildConfig
|
import ir.armor.tachidesk.server.BuildConfig
|
||||||
|
import ir.armor.tachidesk.server.ServerConfig
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
fun openInBrowser() {
|
fun openInBrowser() {
|
||||||
try {
|
try {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,11 +45,11 @@ fun systemTray(): SystemTray? {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
||||||
|
|
||||||
// systemTray.setTooltip("Tachidesk")
|
// systemTray.setTooltip("Tachidesk")
|
||||||
systemTray.setImage(icon)
|
systemTray.setImage(icon)
|
||||||
// systemTray.status = "No Mail"
|
// systemTray.status = "No Mail"
|
||||||
|
|
||||||
mainMenu.add(
|
mainMenu.add(
|
||||||
MenuItem("Quit") {
|
MenuItem("Quit") {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"@fontsource/roboto": "^4.3.0",
|
"@fontsource/roboto": "^4.3.0",
|
||||||
"@material-ui/core": "^4.11.4",
|
"@material-ui/core": "^4.11.4",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"file-selector": "^0.2.4",
|
"file-selector": "^0.2.4",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Container } from '@material-ui/core';
|
|||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import NavBar from './components/NavBar';
|
import NavBar from './components/navbar/NavBar';
|
||||||
import Sources from './screens/Sources';
|
import Sources from './screens/Sources';
|
||||||
import Extensions from './screens/Extensions';
|
import Extensions from './screens/Extensions';
|
||||||
import SourceMangas from './screens/SourceMangas';
|
import SourceMangas from './screens/SourceMangas';
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import FilterListIcon from '@material-ui/icons/FilterList';
|
|||||||
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
import { langCodeToName } from '../util/language';
|
import { langCodeToName } from '../util/language';
|
||||||
|
import cloneObject from '../util/cloneObject';
|
||||||
|
|
||||||
const useStyles = makeStyles(() => createStyles({
|
const useStyles = makeStyles(() => createStyles({
|
||||||
paper: {
|
paper: {
|
||||||
@@ -54,7 +55,7 @@ export default function ExtensionLangSelect(props: IProps) {
|
|||||||
if (checked) {
|
if (checked) {
|
||||||
setMShownLangs([...mShownLangs, lang]);
|
setMShownLangs([...mShownLangs, lang]);
|
||||||
} else {
|
} else {
|
||||||
const clone = JSON.parse(JSON.stringify(mShownLangs));
|
const clone = cloneObject(mShownLangs);
|
||||||
clone.splice(clone.indexOf(lang), 1);
|
clone.splice(clone.indexOf(lang), 1);
|
||||||
setMShownLangs(clone);
|
setMShownLangs(clone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const useStyles = makeStyles({
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
drawerOpen: boolean
|
drawerOpen: boolean
|
||||||
|
|
||||||
setDrawerOpen(state: boolean): void
|
setDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import Slide, { SlideProps } from '@material-ui/core/Slide';
|
||||||
|
import Snackbar from '@material-ui/core/Snackbar';
|
||||||
|
import MuiAlert, { AlertProps, Color as Severity } from '@material-ui/lab/Alert';
|
||||||
|
|
||||||
|
function removeToast(id: string) {
|
||||||
|
const container = document.querySelector(`#${id}`)!!;
|
||||||
|
ReactDOM.unmountComponentAtNode(container);
|
||||||
|
document.body.removeChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Transition(props: SlideProps) {
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <Slide {...props} direction="up" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Alert(props: AlertProps) {
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <MuiAlert elevation={6} variant="filled" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IToastProps{
|
||||||
|
message: string
|
||||||
|
severity: Severity
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toast(props: IToastProps) {
|
||||||
|
const { message, severity } = props;
|
||||||
|
const [open, setOpen] = React.useState(true);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
TransitionComponent={Transition}
|
||||||
|
message="I love snacks"
|
||||||
|
>
|
||||||
|
<MuiAlert elevation={6} variant="filled" onClose={handleClose} severity={severity}>
|
||||||
|
{message}
|
||||||
|
</MuiAlert>
|
||||||
|
</Snackbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function makeToast(message: string, severity: Severity) {
|
||||||
|
const id = Math.floor(Math.random() * 1000);
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = `alert-${id}`;
|
||||||
|
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
ReactDOM.render(<Toast message={message} severity={severity} />, container);
|
||||||
|
|
||||||
|
setTimeout(() => removeToast(container.id), 3500);
|
||||||
|
}
|
||||||
+3
-3
@@ -12,9 +12,9 @@ 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 NavBarContext from '../context/NavbarContext';
|
import NavBarContext from '../../context/NavbarContext';
|
||||||
import DarkTheme from '../context/DarkTheme';
|
import DarkTheme from '../../context/DarkTheme';
|
||||||
import TemporaryDrawer from './TemporaryDrawer';
|
import TemporaryDrawer from '../TemporaryDrawer';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
+113
-103
@@ -23,13 +23,15 @@ import { Switch } from '@material-ui/core';
|
|||||||
import List from '@material-ui/core/List';
|
import List from '@material-ui/core/List';
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import Select from '@material-ui/core/Select';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
||||||
import Collapse from '@material-ui/core/Collapse';
|
import Collapse from '@material-ui/core/Collapse';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||||
import DarkTheme from '../context/DarkTheme';
|
import DarkTheme from '../../context/DarkTheme';
|
||||||
import NavBarContext from '../context/NavbarContext';
|
import NavBarContext from '../../context/NavbarContext';
|
||||||
|
|
||||||
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
||||||
// main container and root div need to change classes...
|
// main container and root div need to change classes...
|
||||||
@@ -44,7 +46,7 @@ const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
|||||||
position: settings.staticNav ? 'sticky' : 'fixed',
|
position: settings.staticNav ? 'sticky' : 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
minWidth: '300px',
|
width: '300px',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
backgroundColor: '#0a0b0b',
|
backgroundColor: '#0a0b0b',
|
||||||
@@ -137,16 +139,12 @@ const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface IReaderSettings{
|
|
||||||
staticNav: boolean
|
|
||||||
showPageNumber: boolean
|
|
||||||
continuesPageGap: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultReaderSettings = () => ({
|
export const defaultReaderSettings = () => ({
|
||||||
staticNav: false,
|
staticNav: false,
|
||||||
showPageNumber: true,
|
showPageNumber: true,
|
||||||
continuesPageGap: false,
|
continuesPageGap: false,
|
||||||
|
loadNextonEnding: false,
|
||||||
|
readerType: 'ContinuesVertical',
|
||||||
} as IReaderSettings);
|
} as IReaderSettings);
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -171,7 +169,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
|
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
|
||||||
const [hideOpenButton, setHideOpenButton] = useState(false);
|
const [hideOpenButton, setHideOpenButton] = useState(false);
|
||||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||||
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(false);
|
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(true);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const classes = useStyles(settings)();
|
const classes = useStyles(settings)();
|
||||||
@@ -205,32 +203,31 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClickAwayListener onClickAway={() => (drawerVisible && setDrawerOpen(false))}>
|
<Slide
|
||||||
<Slide
|
direction="right"
|
||||||
direction="right"
|
in={drawerOpen}
|
||||||
in={drawerOpen}
|
timeout={200}
|
||||||
timeout={200}
|
appear={false}
|
||||||
appear={false}
|
mountOnEnter
|
||||||
mountOnEnter
|
unmountOnExit
|
||||||
unmountOnExit
|
onEntered={() => setDrawerVisible(true)}
|
||||||
onEntered={() => setDrawerVisible(true)}
|
onExited={() => setDrawerVisible(false)}
|
||||||
onExited={() => setDrawerVisible(false)}
|
>
|
||||||
>
|
<div className={classes.root}>
|
||||||
<div className={classes.root}>
|
<header>
|
||||||
<header>
|
<IconButton
|
||||||
<IconButton
|
edge="start"
|
||||||
edge="start"
|
color="inherit"
|
||||||
color="inherit"
|
aria-label="menu"
|
||||||
aria-label="menu"
|
disableRipple
|
||||||
disableRipple
|
onClick={() => history.push(`/manga/${manga.id}`)}
|
||||||
onClick={() => history.push(`/manga/${manga.id}`)}
|
>
|
||||||
>
|
<CloseIcon />
|
||||||
<CloseIcon />
|
</IconButton>
|
||||||
</IconButton>
|
<Typography variant="h1">
|
||||||
<Typography variant="h1">
|
{title}
|
||||||
{title}
|
</Typography>
|
||||||
</Typography>
|
{!settings.staticNav
|
||||||
{!settings.staticNav
|
|
||||||
&& (
|
&& (
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
@@ -242,70 +239,84 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
<KeyboardArrowLeftIcon />
|
<KeyboardArrowLeftIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) }
|
) }
|
||||||
</header>
|
</header>
|
||||||
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
|
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
|
||||||
<ListItemText primary="Reader Settings" />
|
<ListItemText primary="Reader Settings" />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
disableRipple
|
disableRipple
|
||||||
disableFocusRipple
|
disableFocusRipple
|
||||||
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
|
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
|
||||||
|
>
|
||||||
|
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
|
||||||
|
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Static Navigation" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={settings.staticNav}
|
||||||
|
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Show page number" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={settings.showPageNumber}
|
||||||
|
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Load next chapter at ending" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={settings.loadNextonEnding}
|
||||||
|
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Reader Type" />
|
||||||
|
<Select
|
||||||
|
value={settings.readerType}
|
||||||
|
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
||||||
>
|
>
|
||||||
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
|
<MenuItem value="SingleLTR">Left to right</MenuItem>
|
||||||
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
|
<MenuItem value="SingleRTL">Right to left(WIP)</MenuItem>
|
||||||
</IconButton>
|
<MenuItem value="SingleVertical">Vertical(WIP)</MenuItem>
|
||||||
</ListItemSecondaryAction>
|
<MenuItem value="Webtoon">Webtoon</MenuItem>
|
||||||
</ListItem>
|
<MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
|
||||||
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
|
<MenuItem value="ContinuesHorizontal">Horizontal(WIP)</MenuItem>
|
||||||
<List>
|
</Select>
|
||||||
<ListItem>
|
</ListItem>
|
||||||
<ListItemText primary="Static Navigation" />
|
</List>
|
||||||
<ListItemSecondaryAction>
|
</Collapse>
|
||||||
<Switch
|
<hr />
|
||||||
edge="end"
|
<div className={classes.navigation}>
|
||||||
checked={settings.staticNav}
|
<span>
|
||||||
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
Currently on page
|
||||||
/>
|
{' '}
|
||||||
</ListItemSecondaryAction>
|
{curPage + 1}
|
||||||
</ListItem>
|
{' '}
|
||||||
<ListItem>
|
of
|
||||||
<ListItemText primary="Show page number" />
|
{' '}
|
||||||
<ListItemSecondaryAction>
|
{chapter.pageCount}
|
||||||
<Switch
|
</span>
|
||||||
edge="end"
|
<div className={classes.navigationChapters}>
|
||||||
checked={settings.showPageNumber}
|
{chapter.index > 1
|
||||||
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary="Continues Page gap" />
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={settings.continuesPageGap}
|
|
||||||
onChange={(e) => setSettingValue('continuesPageGap', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
<hr />
|
|
||||||
<div className={classes.navigation}>
|
|
||||||
<span>
|
|
||||||
Currently on page
|
|
||||||
{' '}
|
|
||||||
{curPage + 1}
|
|
||||||
{' '}
|
|
||||||
of
|
|
||||||
{' '}
|
|
||||||
{chapter.pageCount}
|
|
||||||
</span>
|
|
||||||
<div className={classes.navigationChapters}>
|
|
||||||
{chapter.index > 1
|
|
||||||
&& (
|
&& (
|
||||||
<Link
|
<Link
|
||||||
style={{ gridArea: 'prev' }}
|
style={{ gridArea: 'prev' }}
|
||||||
@@ -321,7 +332,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{chapter.index < chapter.chapterCount
|
{chapter.index < chapter.chapterCount
|
||||||
&& (
|
&& (
|
||||||
<Link
|
<Link
|
||||||
style={{ gridArea: 'next' }}
|
style={{ gridArea: 'next' }}
|
||||||
@@ -337,11 +348,10 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Slide>
|
</div>
|
||||||
</ClickAwayListener>
|
</Slide>
|
||||||
<Zoom in={!drawerOpen}>
|
<Zoom in={!drawerOpen}>
|
||||||
<Fade in={!hideOpenButton}>
|
<Fade in={!hideOpenButton}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -11,23 +11,26 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import LazyLoad from 'react-lazyload';
|
import LazyLoad from 'react-lazyload';
|
||||||
import { IReaderSettings } from './ReaderNavBar';
|
|
||||||
|
|
||||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
loading: {
|
loading: {
|
||||||
margin: '100px auto',
|
margin: '100px auto',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
},
|
},
|
||||||
loadingImage: {
|
loadingImage: {
|
||||||
padding: settings.staticNav ? 'calc(50vh - 40px) calc(50vw - 340px)' : 'calc(50vh - 40px) calc(50vw - 40px)',
|
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
width: '200px',
|
width: '70vw',
|
||||||
|
padding: '50px calc(50% - 20px)',
|
||||||
backgroundColor: '#525252',
|
backgroundColor: '#525252',
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginBottom: settings.continuesPageGap ? '15px' : 0,
|
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
|
||||||
|
minWidth: '50vw',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ function LazyImage(props: IProps) {
|
|||||||
|
|
||||||
if (imageSrc.length === 0) {
|
if (imageSrc.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={classes.loadingImage}>
|
<div className={`${classes.image} ${classes.loadingImage}`}>
|
||||||
<CircularProgress thickness={5} />
|
<CircularProgress thickness={5} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -85,7 +88,6 @@ function LazyImage(props: IProps) {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={`Page #${index}`}
|
alt={`Page #${index}`}
|
||||||
style={{ width: '100%', maxWidth: '95vw' }}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,22 +100,12 @@ export default function Page(props: IProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: '0 auto' }}>
|
<div style={{ margin: '0 auto' }}>
|
||||||
<LazyLoad
|
<LazyImage
|
||||||
offset={window.innerHeight}
|
src={src}
|
||||||
placeholder={(
|
index={index}
|
||||||
<div className={classes.loading}>
|
setCurPage={setCurPage}
|
||||||
<CircularProgress thickness={5} />
|
settings={settings}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
once
|
|
||||||
>
|
|
||||||
<LazyImage
|
|
||||||
src={src}
|
|
||||||
index={index}
|
|
||||||
setCurPage={setCurPage}
|
|
||||||
settings={settings}
|
|
||||||
/>
|
|
||||||
</LazyLoad>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
|
pageNumber: {
|
||||||
|
display: settings.showPageNumber ? 'block' : 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '50px',
|
||||||
|
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
||||||
|
width: '50px',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
settings: IReaderSettings
|
||||||
|
curPage: number
|
||||||
|
pageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PageNumber(props: IProps) {
|
||||||
|
const { settings, curPage, pageCount } = props;
|
||||||
|
const classes = useStyles(settings)();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.pageNumber}>
|
||||||
|
{`${curPage + 1} / ${pageCount}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React from 'react';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
reader: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
overflowX: 'scroll',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
pages: Array<IReaderPage>
|
||||||
|
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
settings: IReaderSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HorizontalPager(props: IProps) {
|
||||||
|
const { pages, settings, setCurPage } = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.reader}>
|
||||||
|
{
|
||||||
|
pages.map((page) => (
|
||||||
|
<Page
|
||||||
|
key={page.index}
|
||||||
|
index={page.index}
|
||||||
|
src={page.src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
reader: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function PagedReader(props: IReaderProps) {
|
||||||
|
const {
|
||||||
|
pages, settings, setCurPage, curPage, manga, chapter,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
if (curPage < pages.length - 1) {
|
||||||
|
setCurPage(curPage + 1);
|
||||||
|
} else if (settings.loadNextonEnding) {
|
||||||
|
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
if (curPage > 0) { setCurPage(curPage - 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardControl(e:KeyboardEvent) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
nextPage();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
prevPage();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickControl(e:MouseEvent) {
|
||||||
|
if (e.clientX > window.innerWidth / 2) {
|
||||||
|
nextPage();
|
||||||
|
} else {
|
||||||
|
prevPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keyup', keyboardControl, false);
|
||||||
|
pageRef.current?.addEventListener('click', clickControl);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keyup', keyboardControl);
|
||||||
|
pageRef.current?.removeEventListener('click', clickControl);
|
||||||
|
};
|
||||||
|
}, [curPage, pageRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={pageRef} className={classes.reader}>
|
||||||
|
<Page
|
||||||
|
key={curPage}
|
||||||
|
index={curPage}
|
||||||
|
src={pages[curPage].src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
reader: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function VerticalReader(props: IReaderProps) {
|
||||||
|
const {
|
||||||
|
pages, settings, setCurPage, curPage, manga, chapter,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleLoadNextonEnding = () => {
|
||||||
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
|
setCurPage(0);
|
||||||
|
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.loadNextonEnding) { window.addEventListener('scroll', handleLoadNextonEnding); }
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleLoadNextonEnding);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.reader}>
|
||||||
|
{
|
||||||
|
pages.map((page) => (
|
||||||
|
<Page
|
||||||
|
key={page.index}
|
||||||
|
index={page.index}
|
||||||
|
src={page.src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||||||
import MangaGrid from '../components/MangaGrid';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
|
import cloneObject from '../util/cloneObject';
|
||||||
|
|
||||||
interface IMangaCategory {
|
interface IMangaCategory {
|
||||||
category: ICategory
|
category: ICategory
|
||||||
@@ -98,7 +99,7 @@ export default function Library() {
|
|||||||
client.get(`/api/v1/category/${tab.category.id}`)
|
client.get(`/api/v1/category/${tab.category.id}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data: IManga[]) => {
|
.then((data: IManga[]) => {
|
||||||
const tabsClone = JSON.parse(JSON.stringify(tabs));
|
const tabsClone = cloneObject(tabs);
|
||||||
tabsClone[index].mangas = data;
|
tabsClone[index].mangas = data;
|
||||||
tabsClone[index].isFetched = true;
|
tabsClone[index].isFetched = true;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import MangaDetails from '../components/MangaDetails';
|
|||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
import LoadingPlaceholder from '../components/LoadingPlaceholder';
|
import LoadingPlaceholder from '../components/LoadingPlaceholder';
|
||||||
|
import makeToast from '../components/Toast';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -43,10 +44,6 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// const InnerItem = React.memo(({ chapters, index }: any) => (
|
|
||||||
// <ChapterCard chapter={chapters[index]} />
|
|
||||||
// ));
|
|
||||||
|
|
||||||
export default function Manga() {
|
export default function Manga() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -58,6 +55,8 @@ export default function Manga() {
|
|||||||
|
|
||||||
const [manga, setManga] = useState<IManga>();
|
const [manga, setManga] = useState<IManga>();
|
||||||
const [chapters, setChapters] = useState<IChapter[]>([]);
|
const [chapters, setChapters] = useState<IChapter[]>([]);
|
||||||
|
const [fetchedChapters, setFetchedChapters] = useState(false);
|
||||||
|
const [noChaptersFound, setNoChaptersFound] = useState(false);
|
||||||
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
|
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
|
||||||
|
|
||||||
function triggerChaptersUpdate() {
|
function triggerChaptersUpdate() {
|
||||||
@@ -76,19 +75,18 @@ export default function Manga() {
|
|||||||
}, [manga]);
|
}, [manga]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shouldFetchOnline = chapters.length > 0 && chapterUpdateTriggerer === 0;
|
const shouldFetchOnline = fetchedChapters && chapterUpdateTriggerer === 0;
|
||||||
client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`)
|
client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data) => setChapters(data));
|
.then((data) => {
|
||||||
}, [chapters.length, chapterUpdateTriggerer]);
|
if (data.length === 0 && fetchedChapters) {
|
||||||
|
makeToast('No chapters found', 'warning');
|
||||||
// const itemContent = (index:any) => <InnerItem chapters={chapters} index={index} />;
|
setNoChaptersFound(true);
|
||||||
const itemContent = (index:any) => (
|
}
|
||||||
<ChapterCard
|
setChapters(data);
|
||||||
chapter={chapters[index]}
|
})
|
||||||
triggerChaptersUpdate={triggerChaptersUpdate}
|
.then(() => setFetchedChapters(true));
|
||||||
/>
|
}, [chapters.length, fetchedChapters, chapterUpdateTriggerer]);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
@@ -99,7 +97,7 @@ export default function Manga() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
shouldRender={chapters.length > 0}
|
shouldRender={chapters.length > 0 || noChaptersFound}
|
||||||
>
|
>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{ // override Virtuoso default values and set them with class
|
style={{ // override Virtuoso default values and set them with class
|
||||||
@@ -107,7 +105,12 @@ export default function Manga() {
|
|||||||
}}
|
}}
|
||||||
className={classes.chapters}
|
className={classes.chapters}
|
||||||
totalCount={chapters.length}
|
totalCount={chapters.length}
|
||||||
itemContent={itemContent}
|
itemContent={(index:number) => (
|
||||||
|
<ChapterCard
|
||||||
|
chapter={chapters[index]}
|
||||||
|
triggerChaptersUpdate={triggerChaptersUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
useWindowScroll={window.innerWidth < 960}
|
useWindowScroll={window.innerWidth < 960}
|
||||||
overscan={window.innerHeight * 0.5}
|
overscan={window.innerHeight * 0.5}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,36 +10,52 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import Page from '../components/Page';
|
import HorizontalPager from '../components/reader/pager/HorizontalPager';
|
||||||
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
|
import Page from '../components/reader/Page';
|
||||||
|
import PageNumber from '../components/reader/PageNumber';
|
||||||
|
import WebtoonPager from '../components/reader/pager/PagedPager';
|
||||||
|
import VerticalPager from '../components/reader/pager/VerticalPager';
|
||||||
|
import ReaderNavBar, { defaultReaderSettings } from '../components/navbar/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';
|
||||||
|
import cloneObject from '../util/cloneObject';
|
||||||
|
|
||||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
reader: {
|
root: {
|
||||||
display: 'flex',
|
width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw',
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
margin: '0 auto',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loading: {
|
loading: {
|
||||||
margin: '50px auto',
|
margin: '50px auto',
|
||||||
},
|
},
|
||||||
|
|
||||||
pageNumber: {
|
|
||||||
display: settings.showPageNumber ? 'block' : 'none',
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '50px',
|
|
||||||
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
|
||||||
width: '50px',
|
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
||||||
borderRadius: '10px',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getReaderComponent = (readerType: ReaderType) => {
|
||||||
|
switch (readerType) {
|
||||||
|
case 'ContinuesVertical':
|
||||||
|
return VerticalPager;
|
||||||
|
break;
|
||||||
|
case 'Webtoon':
|
||||||
|
return VerticalPager;
|
||||||
|
break;
|
||||||
|
case 'SingleVertical':
|
||||||
|
return WebtoonPager;
|
||||||
|
break;
|
||||||
|
case 'SingleRTL':
|
||||||
|
return WebtoonPager;
|
||||||
|
break;
|
||||||
|
case 'SingleLTR':
|
||||||
|
return WebtoonPager;
|
||||||
|
break;
|
||||||
|
case 'ContinuesHorizontal':
|
||||||
|
return HorizontalPager;
|
||||||
|
default:
|
||||||
|
return VerticalPager;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
const initialChapter = () => ({ pageCount: -1, index: -1, chapterCount: 0 });
|
const initialChapter = () => ({ pageCount: -1, index: -1, chapterCount: 0 });
|
||||||
|
|
||||||
@@ -54,9 +70,22 @@ export default function Reader() {
|
|||||||
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
||||||
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
|
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
|
||||||
const [curPage, setCurPage] = useState<number>(0);
|
const [curPage, setCurPage] = useState<number>(0);
|
||||||
|
|
||||||
const { setOverride, setTitle } = useContext(NavbarContext);
|
const { setOverride, setTitle } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// make sure settings has all the keys
|
||||||
|
const settingsClone = cloneObject(settings) as any;
|
||||||
|
const defualtSettings = defaultReaderSettings();
|
||||||
|
let shouldUpdateSettings = false;
|
||||||
|
Object.keys(defualtSettings).forEach((key) => {
|
||||||
|
const keyOf = key as keyof IReaderSettings;
|
||||||
|
if (settings[keyOf] === undefined) {
|
||||||
|
settingsClone[keyOf] = defualtSettings[keyOf];
|
||||||
|
shouldUpdateSettings = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (shouldUpdateSettings) { setSettings(settingsClone); }
|
||||||
|
|
||||||
|
// set the custom navbar
|
||||||
setOverride(
|
setOverride(
|
||||||
{
|
{
|
||||||
status: true,
|
status: true,
|
||||||
@@ -88,6 +117,7 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChapter(initialChapter);
|
setChapter(initialChapter);
|
||||||
|
setCurPage(0);
|
||||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data:IChapter) => {
|
||||||
@@ -102,20 +132,30 @@ export default function Reader() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pages = range(chapter.pageCount).map((index) => ({
|
||||||
|
index,
|
||||||
|
src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ReaderComponent = getReaderComponent(settings.readerType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.reader}>
|
<div className={classes.root}>
|
||||||
<div className={classes.pageNumber}>
|
<PageNumber
|
||||||
{`${curPage + 1} / ${chapter.pageCount}`}
|
settings={settings}
|
||||||
</div>
|
curPage={curPage}
|
||||||
{range(chapter.pageCount).map((index) => (
|
pageCount={chapter.pageCount}
|
||||||
<Page
|
/>
|
||||||
key={index}
|
<ReaderComponent
|
||||||
index={index}
|
pages={pages}
|
||||||
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`}
|
pageCount={chapter.pageCount}
|
||||||
setCurPage={setCurPage}
|
setCurPage={setCurPage}
|
||||||
settings={settings}
|
curPage={curPage}
|
||||||
/>
|
settings={settings}
|
||||||
))}
|
manga={manga}
|
||||||
|
chapter={chapter}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+30
@@ -87,3 +87,33 @@ interface INavbarOverride {
|
|||||||
status: boolean
|
status: boolean
|
||||||
value: any
|
value: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReaderType =
|
||||||
|
'ContinuesVertical'|
|
||||||
|
'Webtoon' |
|
||||||
|
'SingleVertical' |
|
||||||
|
'SingleRTL' |
|
||||||
|
'SingleLTR' |
|
||||||
|
'ContinuesHorizontal';
|
||||||
|
|
||||||
|
interface IReaderSettings{
|
||||||
|
staticNav: boolean
|
||||||
|
showPageNumber: boolean
|
||||||
|
loadNextonEnding: boolean
|
||||||
|
readerType: ReaderType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReaderPage {
|
||||||
|
index: number
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReaderProps {
|
||||||
|
pages: Array<IReaderPage>
|
||||||
|
pageCount: number
|
||||||
|
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
curPage: number
|
||||||
|
settings: IReaderSettings
|
||||||
|
manga: IMangaCard | IManga
|
||||||
|
chapter: IChapter | IPartialChpter
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
export default function cloneObject<T extends object>(obj: T) {
|
||||||
|
return JSON.parse(JSON.stringify(obj)) as T;
|
||||||
|
}
|
||||||
@@ -1487,6 +1487,17 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
|
|
||||||
|
"@material-ui/lab@^4.0.0-alpha.58":
|
||||||
|
version "4.0.0-alpha.58"
|
||||||
|
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.58.tgz#c7ebb66f49863c5acbb20817163737caa299fafc"
|
||||||
|
integrity sha512-GKHlJqLxUeHH3L3dGQ48ZavYrqGOTXkFkiEiuYMAnAvXAZP4rhMIqeHOPXSUQan4Bd8QnafDcpovOSLnadDmKw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.4.4"
|
||||||
|
"@material-ui/utils" "^4.11.2"
|
||||||
|
clsx "^1.0.4"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^16.8.0 || ^17.0.0"
|
||||||
|
|
||||||
"@material-ui/styles@^4.11.4":
|
"@material-ui/styles@^4.11.4":
|
||||||
version "4.11.4"
|
version "4.11.4"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d"
|
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d"
|
||||||
|
|||||||
Reference in New Issue
Block a user