Compare commits

...

93 Commits

Author SHA1 Message Date
Aria Moradi 6ddb5db57b use HEAD for counting commits
CI Publish / Validate Gradle Wrapper (push) Successful in 10s
CI Publish / Build artifacts and release (push) Failing after 18s
2021-05-23 18:22:35 +04:30
Aria Moradi 4f70cc9283 bump to v0.3.8 2021-05-23 17:27:33 +04:30
Aria Moradi 23b643d637 set default category when adding new manga 2021-05-23 15:28:46 +04:30
Aria Moradi fdfc256c4d Meaningful icons! 2021-05-23 13:48:02 +04:30
Aria Moradi fba56c1b75 replace win64 exe with @Syer10's MSVC build 2021-05-23 12:48:47 +04:30
Aria Moradi 4743bfacf7 [SKIP CI] removing Swing force fixed it for @nar1n 2021-05-21 16:47:46 +04:30
Aria Moradi 2356537f7c try swing 2021-05-21 15:55:37 +04:30
Aria Moradi fa071aee84 refactor github api 2021-05-20 20:41:00 +04:30
Aria Moradi c00ca23a8b put the comment where it should be 2021-05-20 20:27:22 +04:30
Aria Moradi 733b017936 fix webUI not being copied 2021-05-20 19:51:52 +04:30
Aria Moradi 4147f2e368 better comment 2021-05-20 19:21:30 +04:30
Aria Moradi 154b9992eb rewrite without retrofit and kotlin-serialization 2021-05-20 19:20:07 +04:30
Aria Moradi 88b881b043 get rid of guava 2021-05-20 17:56:33 +04:30
Aria Moradi 5d1491fb8c fix package directive 2021-05-20 16:23:13 +04:30
Aria Moradi 3a33196cf1 cleanup dependencies 2021-05-20 16:22:54 +04:30
Aria Moradi fa8e0478da lint file 2021-05-20 13:50:10 +04:30
Aria Moradi 7e7e069244 - Set log level eairlier
- Set AndroidCompat's data root properly
2021-05-20 13:48:33 +04:30
Aria Moradi 18e0d34af0 [SKIP CI] "improvments" 2021-05-20 10:52:57 +04:30
Aria Moradi 3fe3f35483 better commit messages 2021-05-20 10:33:33 +04:30
Aria Moradi cf8e274883 better use of kotlin DSL 2021-05-20 10:24:33 +04:30
Aria Moradi 10dee8b345 improve downloader 2021-05-20 02:36:20 +04:30
Aria Moradi ae8d30593f lint 2021-05-19 23:05:25 +04:30
Aria Moradi 9cde46b5da Fix chpater names, closes #81 2021-05-19 23:03:40 +04:30
Aria Moradi 8e61632155 open the right ip 2021-05-19 17:40:26 +04:30
Aria Moradi e2c4b4cb57 handle when the user runs the app instead of clicking on systemtray 2021-05-19 17:38:33 +04:30
Aria Moradi 326da504ea fix gradle complaning about lint tasks depending on webUI:copyBuild 2021-05-19 17:03:12 +04:30
Aria Moradi c5874a3f10 better chapter looks 2021-05-19 16:50:48 +04:30
Aria Moradi 02802fab97 Application mutex 2021-05-19 16:36:17 +04:30
Aria Moradi 29dea10be2 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 13:42:59 +04:30
Aria Moradi 6bc36193dc open server's location please! 2021-05-19 13:42:18 +04:30
Syer10 81e123388e Fix restore crashing (#90) 2021-05-19 05:29:44 +04:30
Aria Moradi 8ebd7869a5 [SKIP CI] name em 2021-05-19 04:30:21 +04:30
Aria Moradi 7a2f5f13f1 [SKIP CI] name em 2021-05-19 04:28:57 +04:30
Aria Moradi 25d7dad39f build the ref that you have been given! 2021-05-19 04:26:20 +04:30
Aria Moradi c8f8795920 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 03:57:27 +04:30
Aria Moradi 84206a7074 bump to v0.3.7
CI Publish / Validate Gradle Wrapper (push) Successful in 13s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-19 03:52:55 +04:30
Aria Moradi 6fd8b36dca [SKIP CI] links to the new preview repo 2021-05-19 03:45:24 +04:30
Aria Moradi d1500baae1 try with access token 2021-05-19 03:21:47 +04:30
Aria Moradi 045801dd1a push to Suwayomi/Tachidesk-preview 2021-05-19 02:56:46 +04:30
Aria Moradi 14a2cbc793 [SKIP CI] fix typo 2021-05-19 02:52:28 +04:30
Aria Moradi fd385017df [SKIP CI] fix typo 2021-05-19 02:50:22 +04:30
Aria Moradi 9b05954cf2 [SKIP CI] fix typo 2021-05-19 02:49:10 +04:30
Aria Moradi 6aaf636069 [SKIP CI] update for new scripts 2021-05-19 02:47:48 +04:30
Aria Moradi d30e89e5ec update workflows to include both 32-bit and 64-bit windows bundles 2021-05-19 02:42:14 +04:30
Aria Moradi 7acc745478 Merge the two windows bundlers 2021-05-19 02:31:56 +04:30
Aria Moradi 5a9a2d816e 32-bit variant of bundler 2021-05-19 02:26:29 +04:30
Aria Moradi 105f11ed02 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 00:28:56 +04:30
Aria Moradi 3021437a05 new preview system! 2021-05-19 00:28:14 +04:30
Aria Moradi 439602fc03 [SKIP CI] no more preview 2021-05-18 23:42:43 +04:30
Aria Moradi 34e13b9589 [SKIP CI] improve wording... 2021-05-18 23:28:59 +04:30
Aria Moradi 2aab4ae918 [SKIP CI] asking for help! 2021-05-18 23:27:14 +04:30
Aria Moradi 7ef67671a4 print tachidesk info on startup 2021-05-18 22:36:41 +04:30
Syer10 e8df84416c Smarter Chapters and cleanup (#87)
* Smarter Chapters and cleanup

* Fix check
2021-05-18 22:22:15 +04:30
Aria Moradi be930bb68b update to the new scheme 2021-05-18 22:03:18 +04:30
Aria Moradi db52948865 update windows instructions 2021-05-18 21:42:30 +04:30
Aria Moradi d2a72526f6 bump to v0.3.6
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-18 21:40:42 +04:30
Aria Moradi 0a9f57b32b cleanup 2021-05-18 21:38:41 +04:30
Aria Moradi 180f210536 update windows instructions 2021-05-18 21:35:57 +04:30
Aria Moradi c1baa31eed the new and simple way of packaging windows 2021-05-18 21:31:25 +04:30
Aria Moradi cacc97cec7 bump to v0.3.5
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-18 02:39:00 +04:30
Aria Moradi d5691fd81c show last read page on initial load 2021-05-18 02:26:45 +04:30
Aria Moradi 49dc9fe5f6 fix wrong chapter count, abstract next page 2021-05-18 01:10:28 +04:30
Aria Moradi c0b49c7428 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-18 00:45:42 +04:30
Aria Moradi fa345af42d use Dispatchers.IO 2021-05-18 00:43:32 +04:30
Aria Moradi db3cc786a1 rename the job 2021-05-18 00:43:21 +04:30
Aria Moradi fe879ae51d [SKIP CI] update windows instructions 2021-05-18 00:01:04 +04:30
Aria Moradi 2f55460ffb unzip the jre
CI Publish / Validate Gradle Wrapper (push) Successful in 14s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 23:33:54 +04:30
Aria Moradi fbc5bd4642 bump to v0.3.4 2021-05-17 23:29:36 +04:30
Aria Moradi 5e0c7d3c9d ditch packr because it can't load extension jars 2021-05-17 23:28:23 +04:30
Aria Moradi 083996a48d wating on: https://github.com/Kotlin/kotlinx.coroutines/issues/261
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 14:30:59 +04:30
Aria Moradi 9d38f478e3 fix slow manga thumbnails issue, next manga reset page issue 2021-05-17 14:22:24 +04:30
Aria Moradi 57274a0a01 remove unused electron script 2021-05-17 12:32:15 +04:30
Aria Moradi b3b56b7fc8 also build windows package for publish 2021-05-17 12:30:15 +04:30
Forgenn 0b690577da Load next chapter when getting to the last page (#84)
* Load next chapter when scrolling to the bottom (Webtoon, Continues Vertical)

* Load next chapter when scrolling to the bottom (Paged reader)

* Added missing types to IReaderProps

* Move load next chapter when at last page to VerticalReader

* Dependency fix

* Use react history for loading next page
2021-05-17 12:27:14 +04:30
Aria Moradi e9683a3a37 bump to v0.3.3 2021-05-17 12:02:01 +04:30
Aria Moradi f8f67b3eba finish up 2021-05-17 11:59:59 +04:30
Aria Moradi 7b16b082d8 needs kt 2021-05-17 11:55:49 +04:30
Aria Moradi 2a783f0d8e btter folder name 2021-05-17 11:54:33 +04:30
Aria Moradi 42ae32de33 give the correct path 2021-05-17 11:40:38 +04:30
Aria Moradi cec7ddc486 update file permissions 2021-05-17 11:28:26 +04:30
Aria Moradi 9c55fc3868 make windows package with packr 2021-05-17 11:20:24 +04:30
Syer10 104c5a8d83 Code cleanup (#85)
* GC Unused or only used once objects

* Move things around a bit

* Revert some changes

* Fix imports

* Revert about change

* Put back logger

* Private logger

* Revert systemtray

* Move import
2021-05-17 02:48:01 +04:30
Aria Moradi 7450b16742 - Reader -> Pager
- add cloneObject
- add missing copyright notices
2021-05-17 01:38:59 +04:30
Aria Moradi 3ecd0931a1 missing from the previous commit 2021-05-17 01:37:25 +04:30
Aria Moradi 2f2a52ae2f Move navbars 2021-05-17 01:36:31 +04:30
Aria Moradi f464087c30 also handle Errors from java 2021-05-16 23:58:32 +04:30
Aria Moradi 2364960388 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-16 22:15:35 +04:30
Aria Moradi 76be4d64cd update launch4j's jre and make it 64bit 2021-05-16 12:39:05 +04:30
Aria Moradi 7d98e8ce47 [SKIP CI] correct link 2021-05-16 01:53:35 +04:30
Aria Moradi 40831fc681 [SKIP CI] new links 2021-05-16 01:52:01 +04:30
Aria Moradi e38e7ccf26 [SKIP CI] troubleshooting from the wiki 2021-05-16 01:48:49 +04:30
Aria Moradi 98b9e2f2cf [SKIP CI] no need to delete data anymore... 2021-05-16 01:47:33 +04:30
Aria Moradi 4bf3c12f76 fixed when spinner stops just after first offline chapter fetch 2021-05-16 01:07:48 +04:30
93 changed files with 1469 additions and 1175 deletions
+2 -1
View File
@@ -9,7 +9,7 @@
# Gradle wrapper # Gradle wrapper
*.jar binary *.jar binary
# Images # Binary files types
*.webp binary *.webp binary
*.png binary *.png binary
*.jpg binary *.jpg binary
@@ -25,3 +25,4 @@
*.pyc binary *.pyc binary
*.swp binary *.swp binary
*.pdf binary *.pdf binary
*.exe binary
+7 -8
View File
@@ -1,17 +1,16 @@
#!/bin/bash #!/bin/bash
rm -rf preview/*.jar preview/*.zip
cp master/server/build/Tachidesk-*.jar preview cp master/server/build/Tachidesk-*.jar preview
cp master/server/build/Tachidesk-*.zip preview
cd preview cd preview
new_jar_build=$(ls *.jar| tail -1) new_jar_build=$(ls Tachidesk-*.jar)
echo "last jar build file name: $new_jar_build" echo "last jar build file name: $new_jar_build"
cp -f $new_jar_build Tachidesk-latest.jar latest=$(echo $new_jar_build | sed -e's/Tachidesk-\|.jar//g')
rm -rf latest_pointer/*
cp $new_jar_build latest_pointer
latest=$(ls *.jar | tail -n1 | sed -e's/Tachidesk-\|.jar//g')
echo "{ \"latest\": \"$latest\" }" > index.json echo "{ \"latest\": \"$latest\" }" > index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.email "github-actions[bot]@users.noreply.github.com"
@@ -19,7 +18,7 @@ git config --global user.name "github-actions[bot]"
git status git status
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then
git add . git add .
git commit -m "Update preview repository" git commit -m "Updated to $latest"
git push git push
else else
echo "No changes to commit" echo "No changes to commit"
+4 -4
View File
@@ -16,7 +16,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
build: build:
name: Build FatJar name: Build pull request
needs: check_wrapper needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')" if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -27,7 +27,7 @@ jobs:
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
- name: Checkout master branch - name: Checkout pull request
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
@@ -57,12 +57,12 @@ 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
+12 -4
View File
@@ -18,7 +18,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
build: build:
name: Build FatJar name: Build artifacts and deploy preview
needs: check_wrapper needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')" if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -59,21 +59,29 @@ 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 packages
run: |
cd master/scripts
./windows32-bundler.sh
./windows64-bundler.sh
- name: Checkout preview branch - name: Checkout preview branch
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
ref: preview repository: 'Suwayomi/Tachidesk-preview'
ref: main
path: preview path: preview
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
- name: Deploy preview - name: Deploy preview
run: | run: |
+13 -37
View File
@@ -18,7 +18,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
build: build:
name: Build FatJar name: Build artifacts and release
needs: check_wrapper needs: check_wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -28,10 +28,10 @@ jobs:
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
- name: Checkout master branch - name: Checkout ${{ github.ref }}
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
ref: master ref: ${{ github.ref }}
path: master path: master
fetch-depth: 0 fetch-depth: 0
@@ -56,54 +56,30 @@ 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 packages
run: |
cd master/scripts
./windows32-bundler.sh
./windows64-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 }}
+3
View File
@@ -9,3 +9,6 @@ build
server/src/main/resources/react server/src/main/resources/react
server/tmp/ server/tmp/
server/tachiserver-data/ server/tachiserver-data/
# OpenJDK downlaods
OpenJDK*
@@ -7,6 +7,7 @@ package xyz.nulldev.ts.config
* 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 ch.qos.logback.classic.Level
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
@@ -41,21 +42,34 @@ open class ConfigManager {
*/ */
fun loadConfigs(): Config { fun loadConfigs(): Config {
//Load reference configs //Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf") val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf") val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val baseConfig =
ConfigFactory.parseMap(
mapOf(
"ts.server.rootDir" to ApplicationRootDir
)
)
//Load user config //Load user config
val userConfig = val userConfig =
File(ApplicationRootDir, "server.conf").let { File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it) ConfigFactory.parseFile(it)
} }
val config = ConfigFactory.empty() val config = ConfigFactory.empty()
.withFallback(baseConfig)
.withFallback(userConfig) .withFallback(userConfig)
.withFallback(compatConfig) .withFallback(compatConfig)
.withFallback(serverConfig) .withFallback(serverConfig)
.resolve() .resolve()
// set log level early
if (debugLogsEnabled(config)) {
setLogLevel(Level.DEBUG)
}
logger.debug { logger.debug {
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true)) "Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
} }
@@ -0,0 +1,20 @@
package xyz.nulldev.ts.config
/*
* 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 ch.qos.logback.classic.Level
import com.typesafe.config.Config
import mu.KotlinLogging
import org.slf4j.Logger
fun setLogLevel(level: Level) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
}
fun debugLogsEnabled(config: Config)
= System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
-1
View File
@@ -87,7 +87,6 @@ function Dedupe($path)
} }
Dedupe "AndroidCompat/src/main/java" Dedupe "AndroidCompat/src/main/java"
Dedupe "server/src/main/java"
Dedupe "server/src/main/kotlin" Dedupe "server/src/main/kotlin"
Write-Output "Copying Android.jar to library folder..." Write-Output "Copying Android.jar to library folder..."
+6 -9
View File
@@ -20,7 +20,7 @@ fi
# foolproof against running from AndroidCompat dir instead of running from project root # foolproof against running from AndroidCompat dir instead of running from project root
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then if [ "$(basename "$(pwd)")" = "AndroidCompat" ]; then
cd .. cd ..
fi fi
@@ -59,7 +59,7 @@ zip --delete android.jar javax/*
echo "Removing java..." echo "Removing java..."
zip --delete android.jar java/* zip --delete android.jar java/*
echo "Removing overriden classes..." echo "Removing overridden classes..."
zip --delete android.jar android/app/Application.class zip --delete android.jar android/app/Application.class
zip --delete android.jar android/app/Service.class zip --delete android.jar android/app/Service.class
zip --delete android.jar android/net/Uri.class zip --delete android.jar android/net/Uri.class
@@ -68,12 +68,12 @@ zip --delete android.jar android/os/Environment.class
zip --delete android.jar android/text/format/Formatter.class zip --delete android.jar android/text/format/Formatter.class
zip --delete android.jar android/text/Html.class zip --delete android.jar android/text/Html.class
# Dedup overriden Android classes # Dedup overridden Android classes
ABS_JAR="$(realpath android.jar)" ABS_JAR="$(realpath android.jar)"
function dedup() { function dedup() {
pushd "$1" pushd "$1"
CLASSES="$(find * -type f)" CLASSES="$(find ./* -type f)"
echo "$CLASSES" | while read class echo "$CLASSES" | while read -r class
do do
NAME="${class%.*}" NAME="${class%.*}"
echo "Processing class: $NAME" echo "Processing class: $NAME"
@@ -82,13 +82,10 @@ function dedup() {
popd popd
} }
pushd .. popd
dedup AndroidCompat/src/main/java dedup AndroidCompat/src/main/java
dedup server/src/main/java
dedup server/src/main/kotlin dedup server/src/main/kotlin
popd
popd
echo "Copying Android.jar to library folder..." echo "Copying Android.jar to library folder..."
mv tmp/android.jar AndroidCompat/lib mv tmp/android.jar AndroidCompat/lib
+5 -5
View File
@@ -16,7 +16,7 @@ This structure is chosen to
- Eaise development of alternative user intefaces for Tachidesk - Eaise development of alternative user intefaces for Tachidesk
## User Interfaces for Tachidesk server ## User Interfaces for Tachidesk server
Currently there are three known interfaces for Tachidesk: Currently, there are three known interfaces for Tachidesk:
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with. 1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x 2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development. 3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
@@ -28,7 +28,7 @@ You need these software packages installed in order to build the project
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works) - Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Android stubs jar - Android stubs jar
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`. - Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository. - Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### webUI ### webUI
- Nodejs LTS or latest - Nodejs LTS or latest
- Yarn - Yarn
@@ -38,15 +38,15 @@ Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file
### building without `webUI` bundled(server only) ### building without `webUI` bundled(server only)
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`. Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package ### building the Windows package
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`. First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
## Running in development mode ## Running in development mode
First satistify [the prerequisites](#prerequisites) First satisfy [the prerequisites](#prerequisites)
### server ### server
run `./gradlew :server:run --stacktrace` to run the server run `./gradlew :server:run --stacktrace` to run the server
### webUI ### webUI
How to do it is described in `webUI/react/README.md` but for short, How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once) first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the development server, if a new browser window doesn't get opned automatically, then `yarn start` to start the development server, if a new browser window doesn't get opened automatically,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need. and supports HMR and all the other goodies you'll need.
+6 -8
View File
@@ -1,7 +1,7 @@
| Build | Stable | Preview | Support Server | | Build | Stable | Preview | Support Server |
|-------|----------|---------|---------| |-------|----------|---------|---------|
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk/raw/preview/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) | | ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/tree/main/) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
# Tachidesk # Tachidesk
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/> <img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
@@ -14,6 +14,8 @@ Tachidesk is as multi-platform as you can get. Any platform that runs java and/o
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
## Is this application usable? Should I test it? ## Is this application usable? Should I test it?
Here is a list of current features: Here is a list of current features:
@@ -24,7 +26,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
@@ -35,19 +37,15 @@ Download the latest "Stable" jar release from [the releases section](https://git
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) 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` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) 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.
### Windows ### Windows
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases). Download the latest win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
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. 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-win64.zip` and run `Tachidesk Launcher.exe` or `Tachidesk Launcher.bat`. The rest works like the previous section.
### Arch Linux ### Arch Linux
You can install Tachidesk from the AUR You can install Tachidesk from the AUR
``` ```
yay -S tachidesk yay -S tachidesk
``` ```
Or the latest preview version
```
yay -S tachidesk-preview
```
### Docker ### Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile. Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
+2 -2
View File
@@ -59,14 +59,14 @@ configure(projects) {
implementation("ch.qos.logback:logback-classic:1.2.3") implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("io.github.microutils:kotlin-logging:2.0.6") implementation("io.github.microutils:kotlin-logging:2.0.6")
// RxJava // ReactiveX
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxkotlin:1.0.0") implementation("io.reactivex:rxkotlin:1.0.0")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// JSoup // JSoup
implementation("org.jsoup:jsoup:1.13.1") implementation("org.jsoup:jsoup:1.13.1")
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1") implementation("com.typesafe:config:1.4.1")
implementation("io.github.config4k:config4k:0.4.2") implementation("io.github.config4k:config4k:0.4.2")
@@ -0,0 +1 @@
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
start "" jre/bin/javaw -jar Tachidesk.jar
+5
View File
@@ -0,0 +1,5 @@
#include <stdlib.h>
int main() {
system("start jre\\bin\\javaw -jar Tachidesk.jar");
}
+3
View File
@@ -0,0 +1,3 @@
# Building `Tachidesk Launcher.exe`
1. compile `Tachidesk Launcher.c` statically with MSVC compiler.
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
+38
View File
@@ -0,0 +1,38 @@
# 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/.
Write-Output "Downloading jre..."
$jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
if (!(Test-Path $jre)) {
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
}
Write-Output "creating windows bundle"
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
$release_name=$jar.BaseName + "-win32"
# make release dir
New-Item -ItemType Directory $release_name
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
# move jre
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
Copy-Item "resources/Tachidesk Launcher-win32.exe" $release_name
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
$zip_name="$release_name.zip"
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
Remove-Item -Force -Recurse $release_name
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
+41
View File
@@ -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 jre..."
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -o $jre
fi
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)-win32
# make release dir
mkdir $release_name
unzip $jre
# move jre
mv jdk8u292-b10-jre $release_name/jre
cp $jar $release_name/Tachidesk.jar
cp "resources/Tachidesk Launcher-win32.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
mv $zip_name ../server/build/
+38
View File
@@ -0,0 +1,38 @@
# 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/.
Write-Output "Downloading jre..."
$jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
if (!(Test-Path $jre)) {
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
}
Write-Output "creating windows bundle"
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
$release_name=$jar.BaseName + "-win64"
# make release dir
New-Item -ItemType Directory $release_name
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
# move jre
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
Copy-Item "resources/Tachidesk Launcher-win64.exe" $release_name
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
$zip_name="$release_name.zip"
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
Remove-Item -Force -Recurse $release_name
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
+41
View File
@@ -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 jre..."
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
fi
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
# make release dir
mkdir $release_name
unzip $jre
# move jre
mv jdk8u292-b10-jre $release_name/jre
cp $jar $release_name/Tachidesk.jar
cp "resources/Tachidesk Launcher-win64.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
mv $zip_name ../server/build/
+29 -104
View File
@@ -8,53 +8,31 @@ plugins {
application application
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.0.0"
id("org.jmailen.kotlinter") version "3.4.3" id("org.jmailen.kotlinter") version "3.4.3"
id("edu.sc.seis.launch4j") version "2.5.0"
id("de.fuerstenau.buildconfig") version "1.1.8" id("de.fuerstenau.buildconfig") version "1.1.8"
} }
repositories { repositories {
mavenCentral() maven {
url = uri("https://repo1.maven.org/maven2/")
}
maven { maven {
url = uri("https://jitpack.io") url = uri("https://jitpack.io")
} }
} }
dependencies { dependencies {
// Source models and interfaces from Tachiyomi 1.x // okhttp
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
// implementation("tachiyomi.sourceapi:source-api:1.1")
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
val okhttpVersion = "4.10.0-RC1"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0") implementation("com.squareup.okio:okio:2.10.0")
// Javalin api
// Retrofit
val retrofitVersion = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
// Reactivex
implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// api
implementation("io.javalin:javalin:3.13.6") implementation("io.javalin:javalin:3.13.6")
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3") // jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
// Exposed ORM // Exposed ORM
val exposedVersion = "0.31.1" val exposedVersion = "0.31.1"
@@ -62,7 +40,6 @@ dependencies {
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
// current database driver // current database driver
implementation("com.h2database:h2:1.4.200") implementation("com.h2database:h2:1.4.200")
@@ -70,7 +47,19 @@ dependencies {
implementation("com.dorkbox:SystemTray:4.1") implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9") implementation("com.dorkbox:Utilities:1.9")
implementation("com.google.guava:guava:30.1.1-jre")
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidCompat // AndroidCompat
implementation(project(":AndroidCompat")) implementation(project(":AndroidCompat"))
@@ -83,7 +72,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,12 +86,12 @@ sourceSets {
} }
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = "v0.3.2" val tachideskVersion = "v0.3.8"
// counts commit count on master // counts commit count on master
val tachideskRevision = Runtime val tachideskRevision = Runtime
.getRuntime() .getRuntime()
.exec("git rev-list master --count") .exec("git rev-list HEAD --count")
.let { process -> .let { process ->
process.waitFor() process.waitFor()
val output = process.inputStream.use { val output = process.inputStream.use {
@@ -126,18 +115,8 @@ buildConfig {
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString()) buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
} }
launch4j { //used for windows
mainClassName = MainClass
bundledJrePath = "jre"
bundledJre64Bit = true
jreMinVersion = "8"
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
}
tasks { tasks {
jar { shadowJar {
manifest { manifest {
attributes( attributes(
mapOf( mapOf(
@@ -149,9 +128,6 @@ tasks {
) )
) )
} }
}
shadowJar {
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion) archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision) archiveClassifier.set(tachideskRevision)
@@ -165,61 +141,11 @@ tasks {
) )
} }
} }
test { test {
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"))
dependsOn("formatKotlin", "lintKotlin") dependsOn("formatKotlin", "lintKotlin")
@@ -235,11 +161,10 @@ tasks {
} }
withType<LintTask> { withType<LintTask> {
source(files("src")) source(files("src/kotlin"))
} }
withType<FormatTask> { withType<FormatTask> {
source(files("src")) source(files("src/kotlin"))
} }
} }
@@ -1,53 +0,0 @@
package eu.kanade.tachiyomi.extension.api
/*
* 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 eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
suspend fun findExtensions(): List<Extension.Available> {
val service: ExtensionGithubService = ExtensionGithubService.create()
val response = service.getRepo()
return parseResponse(response)
}
fun getApkUrl(extension: ExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
}
@@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.extension.api
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
interface ExtensionGithubService {
companion object {
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.build()
}
@ExperimentalSerializationApi
fun create(): ExtensionGithubService {
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.client(client)
.build()
return adapter.create(ExtensionGithubService::class.java)
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}/index.json.gz")
suspend fun getRepo(): JsonArray
}
@@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.extension.model
import eu.kanade.tachiyomi.source.Source
sealed class Extension {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Int
abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val sources: List<Source>,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false
) : Extension()
data class Available(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
) : Extension()
data class Untrusted(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension()
}
@@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.extension.model
enum class InstallStep {
Pending, Downloading, Installing, Installed, Error;
fun isCompleted(): Boolean {
return this == Installed || this == Error
}
}
@@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.extension.model
sealed class LoadResult {
class Success(val extension: Extension.Installed) : LoadResult()
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
class Error(val message: String? = null) : LoadResult() {
constructor(exception: Throwable) : this(exception.message)
}
}
@@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.extension.util
/*
* 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 android.annotation.SuppressLint
// import android.content.Context
// import android.content.pm.PackageInfo
// import android.content.pm.PackageManager
// import dalvik.system.PathClassLoader
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import eu.kanade.tachiyomi.util.lang.Hash
// import kotlinx.coroutines.async
// import kotlinx.coroutines.runBlocking
// import timber.log.Timber
// import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
// @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader {
// private val preferences: PreferencesHelper by injectLazy()
// private val allowNsfwSource by lazy {
// preferences.allowNsfwSource().get()
// }
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
// var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
// fun loadExtensions(context: Context): List<LoadResult> {
// val pkgManager = context.packageManager
// val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
// val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
//
// if (extPkgs.isEmpty()) return emptyList()
//
// // Load each extension concurrently and wait for completion
// return runBlocking {
// val deferred = extPkgs.map {
// async { loadExtension(context, it.packageName, it) }
// }
// deferred.map { it.await() }
// }
// }
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
// fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
// val pkgInfo = try {
// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
// } catch (error: PackageManager.NameNotFoundException) {
// // Unlikely, but the package may have been uninstalled at this point
// return LoadResult.Error(error)
// }
// if (!isPackageAnExtension(pkgInfo)) {
// return LoadResult.Error("Tried to load a package that wasn't a extension")
// }
// return loadExtension(context, pkgName, pkgInfo)
// }
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
// private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
// val pkgManager = context.packageManager
//
// val appInfo = try {
// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
// } catch (error: PackageManager.NameNotFoundException) {
// // Unlikely, but the package may have been uninstalled at this point
// return LoadResult.Error(error)
// }
//
// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
// val versionName = pkgInfo.versionName
// val versionCode = pkgInfo.versionCode
//
// if (versionName.isNullOrEmpty()) {
// val exception = Exception("Missing versionName for extension $extName")
// Timber.w(exception)
// return LoadResult.Error(exception)
// }
//
// // Validate lib version
// val libVersion = versionName.substringBeforeLast('.').toDouble()
// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
// val exception = Exception(
// "Lib version is $libVersion, while only versions " +
// "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
// )
// Timber.w(exception)
// return LoadResult.Error(exception)
// }
//
// val signatureHash = getSignatureHash(pkgInfo)
//
// if (signatureHash == null) {
// return LoadResult.Error("Package $pkgName isn't signed")
// } else if (signatureHash !in trustedSignatures) {
// val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
// Timber.w("Extension $pkgName isn't trusted")
// return LoadResult.Untrusted(extension)
// }
//
// val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
// return LoadResult.Error("NSFW extension $pkgName not allowed")
// }
//
// val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
//
// val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
// .split(";")
// .map {
// val sourceClass = it.trim()
// if (sourceClass.startsWith(".")) {
// pkgInfo.packageName + sourceClass
// } else {
// sourceClass
// }
// }
// .flatMap {
// try {
// when (val obj = Class.forName(it, false, classLoader).newInstance()) {
// is Source -> listOf(obj)
// is SourceFactory -> {
// if (isSourceNsfw(obj)) {
// emptyList()
// } else {
// obj.createSources()
// }
// }
// else -> throw Exception("Unknown source class type! ${obj.javaClass}")
// }
// } catch (e: Throwable) {
// Timber.e(e, "Extension load error: $extName.")
// return LoadResult.Error(e)
// }
// }
// .filter { !isSourceNsfw(it) }
//
// val langs = sources.filterIsInstance<CatalogueSource>()
// .map { it.lang }
// .toSet()
// val lang = when (langs.size) {
// 0 -> ""
// 1 -> langs.first()
// else -> "all"
// }
//
// val extension = Extension.Installed(
// extName,
// pkgName,
// versionName,
// versionCode,
// lang,
// isNsfw,
// sources,
// isUnofficial = signatureHash != officialSignature
// )
// return LoadResult.Success(extension)
// }
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
// private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
// return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
// }
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
// private fun getSignatureHash(pkgInfo: PackageInfo): String? {
// val signatures = pkgInfo.signatures
// return if (signatures != null && signatures.isNotEmpty()) {
// Hash.sha256(signatures.first().toByteArray())
// } else {
// null
// }
// }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
// private fun isSourceNsfw(clazz: Any): Boolean {
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
// return false
// }
//
// if (clazz !is Source && clazz !is SourceFactory) {
// return false
// }
//
// // Annotations are proxied, hence this janky way of checking for them
// return clazz.javaClass.annotations
// .flatMap { it.javaClass.interfaces.map { it.simpleName } }
// .firstOrNull { it == Nsfw::class.java.simpleName } != null
// }
}
@@ -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()
}
}
} }
@@ -35,16 +35,16 @@ object Category {
} }
} }
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) { fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
transaction { transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) { CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null) it[CategoryTable.name] = name if (name != null) it[CategoryTable.name] = name
if (isLanding != null) it[CategoryTable.isLanding] = isLanding if (isDefault != null) it[CategoryTable.isDefault] = isDefault
} }
} }
} }
/** /**
* Move the category from position `from` to `to` * Move the category from position `from` to `to`
*/ */
fun reorderCategory(categoryId: Int, from: Int, to: Int) { fun reorderCategory(categoryId: Int, from: Int, to: Int) {
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.Manga.getManga import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.database.table.ChapterTable import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable import ir.armor.tachidesk.model.database.table.PageTable
@@ -22,102 +22,110 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
object Chapter { object Chapter {
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List<ChapterDataClass> { suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> {
return if (!onlineFetch) { return if (onlineFetch == true) {
getSourceChapters(mangaId)
} else {
transaction { transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC) ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
.map { .map {
ChapterTable.toDataClass(it) ChapterTable.toDataClass(it)
} }
}.ifEmpty {
// If it was explicitly set to offline dont grab chapters
if (onlineFetch == null) {
getSourceChapters(mangaId)
} else emptyList()
} }
} else { }
}
val mangaDetails = getManga(mangaId) private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val source = getHttpSource(mangaDetails.sourceId.toLong()) val mangaDetails = getManga(mangaId)
val chapterList = source.fetchChapterList( val source = getHttpSource(mangaDetails.sourceId.toLong())
SManga.create().apply { val chapterList = source.fetchChapterList(
title = mangaDetails.title SManga.create().apply {
url = mangaDetails.url title = mangaDetails.title
} url = mangaDetails.url
).awaitSingle() }
).awaitSingle()
val chapterCount = chapterList.count() val chapterCount = chapterList.count()
transaction { transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter -> chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) { if (chapterEntry == null) {
ChapterTable.insert { ChapterTable.insert {
it[url] = fetchedChapter.url it[url] = fetchedChapter.url
it[name] = fetchedChapter.name it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1 it[chapterIndex] = index + 1
it[manga] = mangaId it[manga] = mangaId
} }
} else { } else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
it[name] = fetchedChapter.name it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1 it[chapterIndex] = index + 1
it[manga] = mangaId it[manga] = mangaId
}
} }
} }
} }
}
// clear any orphaned chapters that are in the db but not in `chapterList` // clear any orphaned chapters that are in the db but not in `chapterList`
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() } val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
if (dbChapterCount > chapterCount) { // we got some clean up due if (dbChapterCount > chapterCount) { // we got some clean up due
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } } val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
dbChapterList.forEach { dbChapterList.forEach {
if (it[ChapterTable.chapterIndex] >= chapterList.size || if (it[ChapterTable.chapterIndex] >= chapterList.size ||
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url] chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
) { ) {
transaction { transaction {
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] } PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] } ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
}
} }
} }
} }
}
val dbChapterMap = transaction { val dbChapterMap = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId } ChapterTable.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it }) .associateBy({ it[ChapterTable.url] }, { it })
} }
return chapterList.mapIndexed { index, it -> return chapterList.mapIndexed { index, it ->
val dbChapter = dbChapterMap.getValue(it.url) val dbChapter = dbChapterMap.getValue(it.url)
ChapterDataClass( ChapterDataClass(
it.url, it.url,
it.name, it.name,
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId, mangaId,
dbChapter[ChapterTable.isRead], dbChapter[ChapterTable.isRead],
dbChapter[ChapterTable.isBookmarked], dbChapter[ChapterTable.isBookmarked],
dbChapter[ChapterTable.lastPageRead], dbChapter[ChapterTable.lastPageRead],
chapterCount - index, chapterCount - index,
) chapterList.size
} )
} }
} }
@@ -126,9 +134,9 @@ object Chapter {
val chapterEntry = transaction { val chapterEntry = transaction {
ChapterTable.select { ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!! }.first()
} }
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList( val pageList = source.fetchPageList(
@@ -139,7 +147,7 @@ object Chapter {
).awaitSingle() ).awaitSingle()
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() } val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
// update page list for this chapter // update page list for this chapter
transaction { transaction {
@@ -9,25 +9,37 @@ package ir.armor.tachidesk.impl
import ir.armor.tachidesk.impl.Manga.getManga import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.model.database.table.CategoryMangaTable import ir.armor.tachidesk.model.database.table.CategoryMangaTable
import ir.armor.tachidesk.model.database.table.CategoryTable
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.toDataClass import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.model.dataclass.MangaDataClass
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
object Library { object Library {
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets, // TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
// ..implement that shit at some time... // ..implement that shit at some time...
// ..also Consider to rename it to `isDefault` // ..also Consider to rename it to `isDefault`
suspend fun addMangaToLibrary(mangaId: Int) { suspend fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
if (!manga.inLibrary) { if (!manga.inLibrary) {
transaction { transaction {
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true it[MangaTable.inLibrary] = true
it[MangaTable.defaultCategory] = defaultCategories.isEmpty()
}
defaultCategories.forEach { category ->
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = category[CategoryTable.id].value
it[CategoryMangaTable.manga] = mangaId
}
} }
} }
} }
@@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
import ir.armor.tachidesk.impl.Source.getSource import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.await import ir.armor.tachidesk.impl.util.await
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.model.database.table.MangaStatus import ir.armor.tachidesk.model.database.table.MangaStatus
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.model.dataclass.MangaDataClass
@@ -37,7 +37,7 @@ object Manga {
} }
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) { return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
MangaDataClass( MangaDataClass(
@@ -78,14 +78,14 @@ object Manga {
it[MangaTable.description] = truncate(fetchedManga.description, 4096) it[MangaTable.description] = truncate(fetchedManga.description, 4096)
it[MangaTable.genre] = fetchedManga.genre it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
} }
} }
clearMangaThumbnail(mangaId) clearMangaThumbnail(mangaId)
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
MangaDataClass( MangaDataClass(
mangaId, mangaId,
@@ -117,7 +117,7 @@ object Manga {
return getCachedImageResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
getManga(mangaId) // make sure is initialized getManga(mangaId) // make sure is initialized
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val sourceId = mangaEntry[MangaTable.sourceReference] val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
@@ -130,7 +130,7 @@ object Manga {
} }
} }
suspend fun clearMangaThumbnail(mangaId: Int) { private fun clearMangaThumbnail(mangaId: Int) {
val saveDir = applicationDirs.thumbnailsRoot val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
@@ -9,7 +9,7 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaStatus import ir.armor.tachidesk.model.database.table.MangaStatus
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.model.dataclass.MangaDataClass
@@ -40,7 +40,7 @@ object MangaList {
val mangasPage = this val mangasPage = this
val mangaList = transaction { val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga -> return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() val mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
if (mangaEntry == null) { // create manga entry if (mangaEntry == null) { // create manga entry
val mangaId = MangaTable.insertAndGetId { val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url it[url] = manga.url
@@ -9,13 +9,13 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.storage.SafePath
import ir.armor.tachidesk.model.database.table.ChapterTable import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable import ir.armor.tachidesk.model.database.table.PageTable
import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs import ir.armor.tachidesk.server.ApplicationDirs
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -28,7 +28,7 @@ import java.io.File
import java.io.InputStream import java.io.InputStream
object Page { object Page {
/** /**
* A page might have a imageUrl ready from the get go, or we might need to * A page might have a imageUrl ready from the get go, or we might need to
* go an extra step and call fetchImageUrl to get it. * go an extra step and call fetchImageUrl to get it.
*/ */
@@ -40,16 +40,16 @@ object Page {
} }
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> { suspend fun getPageImage(mangaId: Int, chapterIndex: 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 }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
ChapterTable.select { ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!! }.first()
} }
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterEntry[ChapterTable.id].value
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) }.first() }
val tachiPage = Page( val tachiPage = Page(
pageEntry[PageTable.index], pageEntry[PageTable.index],
@@ -68,33 +68,28 @@ object Page {
val saveDir = getChapterDir(mangaId, chapterId) val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs() File(saveDir).mkdirs()
val fileName = index.toString() val fileName = String.format("%03d", index) // e.g. 001.jpeg
return getCachedImageResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).awaitSingle() source.fetchImage(tachiPage).awaitSingle()
} }
} }
// TODO: rewrite this to match tachiyomi
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getChapterDir(mangaId: Int, chapterId: Int): String { private 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 }.first() }
val sourceId = mangaEntry[MangaTable.sourceReference] val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = getHttpSource(sourceId) val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val chapterDir = when { val sourceDir = source.toString()
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}" val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
else -> chapterEntry[ChapterTable.name] val chapterDir = SafePath.buildValidFilename(
} when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
)
val mangaTitle = mangaEntry[MangaTable.title] return "${applicationDirs.mangaRoot}/$sourceDir/$mangaDir/$chapterDir"
val sourceName = source.toString()
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
} }
} }
@@ -9,11 +9,11 @@ package ir.armor.tachidesk.impl
import ir.armor.tachidesk.impl.MangaList.processEntries import ir.armor.tachidesk.impl.MangaList.processEntries
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
object Search { object Search {
// TODO // TODO
fun sourceFilters(sourceId: Long) { fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
// source.getFilterList().toItems() // source.getFilterList().toItems()
@@ -34,7 +34,7 @@ object Search {
val filter: Any val filter: Any
) )
/** /**
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal * Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
*/ */
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> { // private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
@@ -7,7 +7,7 @@ package ir.armor.tachidesk.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.model.database.table.ExtensionTable import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.database.table.SourceTable import ir.armor.tachidesk.model.database.table.SourceTable
@@ -21,7 +21,7 @@ import ir.armor.tachidesk.impl.backup.models.MangaImpl
import ir.armor.tachidesk.impl.backup.models.Track import ir.armor.tachidesk.impl.backup.models.Track
import ir.armor.tachidesk.impl.backup.models.TrackImpl import ir.armor.tachidesk.impl.backup.models.TrackImpl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
@@ -64,11 +64,7 @@ object LegacyBackupImport : LegacyBackupBase() {
logger.info { logger.info {
""" """
Restore Errors: Restore Errors:
${ ${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
errors.map {
"${it.first} - ${it.second}"
}.joinToString("\n")
}
Restore Summary: Restore Summary:
- Missing Sources: - Missing Sources:
${validationResult.missingSources.joinToString("\n")} ${validationResult.missingSources.joinToString("\n")}
@@ -119,6 +115,8 @@ object LegacyBackupImport : LegacyBackupBase() {
getHttpSource(manga.source) getHttpSource(manga.source)
} catch (e: NullPointerException) { } catch (e: NullPointerException) {
null null
} catch (e: NoSuchElementException) {
null
} }
val sourceName = sourceMapping[manga.source] ?: manga.source.toString() val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
@@ -198,7 +196,7 @@ object LegacyBackupImport : LegacyBackupBase() {
it[description] = fetchedManga.description it[description] = fetchedManga.description
it[genre] = fetchedManga.genre it[genre] = fetchedManga.genre
it[status] = fetchedManga.status it[status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
} }
} }
@@ -0,0 +1,28 @@
package ir.armor.tachidesk.impl.download
import org.jetbrains.exposed.sql.ResultRow
import java.util.concurrent.LinkedBlockingQueue
/*
* 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/. */
data class Download(
val chapter: ResultRow,
)
private val downloadQueue = LinkedBlockingQueue<Download>()
class Downloader {
fun start() {
TODO()
}
fun stop() {
TODO()
}
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package ir.armor.tachidesk.impl.extension
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -8,14 +8,13 @@ package ir.armor.tachidesk.impl
* 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 android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass import ir.armor.tachidesk.impl.extension.ExtensionsList.extensionTableAsDataClass
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
@@ -27,6 +26,7 @@ import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
import ir.armor.tachidesk.impl.util.await import ir.armor.tachidesk.impl.util.await
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.model.database.table.ExtensionTable import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.database.table.SourceTable import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs import ir.armor.tachidesk.server.ApplicationDirs
@@ -159,7 +159,7 @@ object Extension {
it[this.classFQName] = className it[this.classFQName] = className
} }
val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!![ExtensionTable.id].value val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first()[ExtensionTable.id].value
sources.forEach { httpSource -> sources.forEach { httpSource ->
SourceTable.insert { SourceTable.insert {
@@ -195,7 +195,7 @@ object Extension {
fun uninstallExtension(pkgName: String) { fun uninstallExtension(pkgName: String) {
logger.debug("Uninstalling $pkgName") logger.debug("Uninstalling $pkgName")
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! } val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first() }
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk") val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction { transaction {
@@ -234,7 +234,7 @@ object Extension {
} }
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> { suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl] val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon" val saveDir = "${applicationDirs.extensionsRoot}/icon"
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package ir.armor.tachidesk.impl.extension
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -7,9 +7,9 @@ package ir.armor.tachidesk.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
import eu.kanade.tachiyomi.extension.model.Extension import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl import ir.armor.tachidesk.impl.extension.github.OnlineExtension
import ir.armor.tachidesk.model.database.table.ExtensionTable import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import mu.KotlinLogging import mu.KotlinLogging
@@ -25,7 +25,7 @@ object ExtensionsList {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
var lastUpdateCheck: Long = 0 var lastUpdateCheck: Long = 0
var updateMap = ConcurrentHashMap<String, Extension.Available>() var updateMap = ConcurrentHashMap<String, OnlineExtension>()
/** 60,000 milliseconds = 60 seconds */ /** 60,000 milliseconds = 60 seconds */
private const val ExtensionUpdateDelayTime = 60 * 1000 private const val ExtensionUpdateDelayTime = 60 * 1000
@@ -63,7 +63,7 @@ object ExtensionsList {
} }
} }
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) { private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
transaction { transaction {
foundExtensions.forEach { foundExtension -> foundExtensions.forEach { foundExtension ->
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull() val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
@@ -0,0 +1,119 @@
package ir.armor.tachidesk.impl.extension.github
/*
* 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 com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.RealResponseBody
import okio.GzipSource
import okio.buffer
import uy.kohesive.injekt.injectLazy
import java.io.IOException
object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private const val LIB_VERSION_MIN = "1.2"
private const val LIB_VERSION_MAX = "1.2"
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
return json
.map { it.asJsonObject }
.filter { element ->
val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.')
libVersion == LIB_VERSION_MAX
}
.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
val versionCode = element["code"].int
val lang = element["lang"].string
val nsfw = element["nsfw"].int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
suspend fun findExtensions(): List<OnlineExtension> {
val response = getRepo()
return parseResponse(response)
}
fun getApkUrl(extension: ExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.addInterceptor(UnzippingInterceptor())
.build()
}
private fun getRepo(): com.google.gson.JsonArray {
val request = Request.Builder()
.url("$REPO_URL_PREFIX/index.json.gz")
.build()
val response = client.newCall(request).execute().use { response -> response.body!!.string() }
return JsonParser.parseString(response).asJsonArray
}
}
// ref: https://stackoverflow.com/questions/51901333/okhttp-3-how-to-decompress-gzip-deflate-response-manually-using-java-android
private class UnzippingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Chain): Response {
val response: Response = chain.proceed(chain.request())
return unzip(response)
}
@Throws(IOException::class)
private fun unzip(response: Response): Response {
if (response.body == null) {
return response
}
// check if we have gzip response
val contentEncoding: String? = response.headers["Content-Encoding"]
// this is used to decompress gzipped responses
return if (contentEncoding != null && contentEncoding == "gzip") {
val body = response.body!!
val contentLength: Long = body.contentLength()
val responseBody = GzipSource(body.source())
val strippedHeaders: Headers = response.headers.newBuilder().build()
response.newBuilder().headers(strippedHeaders)
.body(RealResponseBody(body.contentType().toString(), contentLength, responseBody.buffer()))
.build()
} else {
response
}
}
}
@@ -0,0 +1,12 @@
package ir.armor.tachidesk.impl.extension.github
data class OnlineExtension(
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
)
@@ -32,12 +32,12 @@ object GetHttpSource {
} }
val sourceRecord = transaction { val sourceRecord = transaction {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! SourceTable.select { SourceTable.id eq sourceId }.first()
} }
val extensionId = sourceRecord[SourceTable.extension] val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = transaction { val extensionRecord = transaction {
ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!! ExtensionTable.select { ExtensionTable.id eq extensionId }.first()
} }
val apkName = extensionRecord[ExtensionTable.apkName] val apkName = extensionRecord[ExtensionTable.apkName]
@@ -1,99 +0,0 @@
package ir.armor.tachidesk.impl.util
/*
* 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 eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.FormBody
import okhttp3.OkHttpClient
import java.net.URLEncoder
// TODO: finish MangaDex support
class MangaDexHelper(private val mangaDexSource: HttpSource) {
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
private fun clientBuilder(
r18Toggle: Int,
okHttpClient: OkHttpClient = mangaDexSource.network.client
): OkHttpClient = okHttpClient.newBuilder()
.addNetworkInterceptor { chain ->
val originalCookies = chain.request().header("Cookie") ?: ""
val newReq = chain
.request()
.newBuilder()
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
.build()
chain.proceed(newReq)
}.build()
private fun cookiesHeader(r18Toggle: Int): String {
val cookies = mutableMapOf<String, String>()
cookies["mangadex_h_toggle"] = r18Toggle.toString()
return buildCookies(cookies)
}
private fun buildCookies(cookies: Map<String, String>) =
cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
// fun isLogged(): Boolean {
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
// }
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = clientBuilder().newCall(
POST(
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
mangaDexSource.headers,
formBody.build()
)
).execute()
return response.body!!.string().isEmpty()
}
//
// fun logout(): Boolean {
// return withContext(Dispatchers.IO) {
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
// val listOfDexCookies = network.cookieManager.get(httpUrl)
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
// val token = cookie?.value
// if (token.isNullOrEmpty()) {
// return@withContext true
// }
// val result = clientBuilder().newCall(
// POSTWithCookie(
// "$baseUrl/ajax/actions.ajax.php?function=logout",
// REMEMBER_ME,
// token,
// headers
// )
// ).execute()
// val resultStr = result.body!!.string()
// if (resultStr.contains("success", true)) {
// network.cookieManager.remove(httpUrl)
// return@withContext true
// }
//
// false
// }
// }
}
@@ -11,6 +11,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import java.io.IOException import java.io.IOException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@@ -26,7 +27,9 @@ suspend fun Call.await(): Response {
return return
} }
continuation.resume(response) continuation.resume(response) {
response.body?.closeQuietly()
}
} }
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
@@ -71,12 +71,14 @@ object PackageTools {
if (handler.hasException()) { if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt") val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error( logger.error(
"Detail Error Information in File $errorFile\n" + """
"Please report this file to one of following link if possible (any one).\n" + Detail Error Information in File $errorFile
" https://sourceforge.net/p/dex2jar/tickets/\n" + Please report this file to one of following link if possible (any one).
" https://bitbucket.org/pxb1988/dex2jar/issues\n" + https://sourceforge.net/p/dex2jar/tickets/
" https://github.com/pxb1988/dex2jar/issues\n" + https://bitbucket.org/pxb1988/dex2jar/issues
" dex2jar@googlegroups.com" https://github.com/pxb1988/dex2jar/issues
dex2jar@googlegroups.com
""".trimIndent()
) )
handler.dump(errorFile, emptyArray<String>()) handler.dump(errorFile, emptyArray<String>())
} }
@@ -98,18 +100,21 @@ object PackageTools {
applicationInfo.metaData = Bundle().apply { applicationInfo.metaData = Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0) val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()?.filter { appTag?.childNodes?.toList()
it.nodeType == Node.ELEMENT_NODE .orEmpty()
}?.map { .asSequence()
it as Element .filter {
}?.filter { it.nodeType == Node.ELEMENT_NODE
it.tagName == "meta-data" }.map {
}?.map { it as Element
putString( }.filter {
it.attributes.getNamedItem("android:name").nodeValue, it.tagName == "meta-data"
it.attributes.getNamedItem("android:value").nodeValue }.forEach {
) putString(
} it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
} }
signatures = ( signatures = (
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.util package ir.armor.tachidesk.impl.util.lang
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.util package ir.armor.tachidesk.impl.util.storage
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -8,23 +8,19 @@ package ir.armor.tachidesk.impl.util
* 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 okhttp3.Response import okhttp3.Response
import okio.buffer
import okio.sink
import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
object CachedImageResponse { object CachedImageResponse {
private fun pathToInputStream(path: String): InputStream { private fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path)) return FileInputStream(path).buffered()
} }
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? { private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file -> val target = "$fileName."
if (file.name.startsWith("$fileName.")) File(directoryPath).listFiles().orEmpty().forEach { file ->
if (file.name.startsWith(target))
return "$directoryPath/${file.name}" return "$directoryPath/${file.name}"
} }
return null return null
@@ -45,27 +41,25 @@ object CachedImageResponse {
val response = fetcher() val response = fetcher()
if (response.code == 200) { if (response.code == 200) {
val contentType = response.headers["content-type"]!! val fullPath = "$filePath.tmp"
val fullPath = filePath + "." + contentType.substringAfter("image/") val saveFile = File(fullPath)
response.body!!.source().saveTo(saveFile)
Files.newOutputStream(Paths.get(fullPath)).use { output -> // find image type
response.body!!.source().use { input -> val imageType = response.headers["content-type"]
output.sink().buffer().use { ?: ImageUtil.findImageType { saveFile.inputStream() }?.mime
it.writeAll(input) ?: "image/jpeg"
it.flush() .substringAfter("image/")
}
} saveFile.renameTo(File("$filePath.$imageType"))
}
return Pair( return pathToInputStream(fullPath) to imageType
pathToInputStream(fullPath),
contentType
)
} else { } else {
throw Exception("request error! ${response.code}") throw Exception("request error! ${response.code}")
} }
} }
suspend fun clearCachedImage(saveDir: String, fileName: String) { fun clearCachedImage(saveDir: String, fileName: String) {
val cachedFile = findFileNameStartingWith(saveDir, fileName) val cachedFile = findFileNameStartingWith(saveDir, fileName)
cachedFile?.also { cachedFile?.also {
File(it).delete() File(it).delete()
@@ -0,0 +1,69 @@
package ir.armor.tachidesk.impl.util.storage
import java.io.InputStream
/*
* 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/. */
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
object ImageUtil {
fun findImageType(openStream: () -> InputStream): ImageType? {
return openStream().use { findImageType(it) }
}
fun findImageType(stream: InputStream): ImageType? {
try {
val bytes = ByteArray(8)
val length = if (stream.markSupported()) {
stream.mark(bytes.size)
stream.read(bytes, 0, bytes.size).also { stream.reset() }
} else {
stream.read(bytes, 0, bytes.size)
}
if (length == -1) {
return null
}
if (bytes.compareWith(charByteArrayOf(0xFF, 0xD8, 0xFF))) {
return ImageType.JPG
}
if (bytes.compareWith(charByteArrayOf(0x89, 0x50, 0x4E, 0x47))) {
return ImageType.PNG
}
if (bytes.compareWith("GIF8".toByteArray())) {
return ImageType.GIF
}
if (bytes.compareWith("RIFF".toByteArray())) {
return ImageType.WEBP
}
} catch (e: Exception) {
}
return null
}
private fun ByteArray.compareWith(magic: ByteArray): Boolean {
return magic.indices.none { this[it] != magic[it] }
}
private fun charByteArrayOf(vararg bytes: Int): ByteArray {
return ByteArray(bytes.size).apply {
for (i in bytes.indices) {
set(i, bytes[i].toByte())
}
}
}
enum class ImageType(val mime: String) {
JPG("image/jpeg"),
PNG("image/png"),
GIF("image/gif"),
WEBP("image/webp")
}
}
@@ -0,0 +1,48 @@
package ir.armor.tachidesk.impl.util.storage
/*
* 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 okio.BufferedSource
import okio.buffer
import okio.sink
import java.io.File
import java.io.OutputStream
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt
/**
* Saves the given source to a file and closes it. Directories will be created if needed.
*
* @param file the file where the source is copied.
*/
fun BufferedSource.saveTo(file: File) {
try {
// Create parent dirs if needed
file.parentFile.mkdirs()
// Copy to destination
saveTo(file.outputStream())
} catch (e: Exception) {
close()
file.delete()
throw e
}
}
/**
* Saves the given source to an output stream and closes both resources.
*
* @param stream the stream where the source is copied.
*/
fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
it.flush()
}
}
}
@@ -0,0 +1,47 @@
package ir.armor.tachidesk.impl.util.storage
/*
* 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/. */
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
object SafePath {
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
* with a dot), but you can manually add it later.
*/
fun buildValidFilename(origName: String): String {
val name = origName.trim('.', ' ')
if (name.isEmpty()) {
return "(invalid)"
}
val sb = StringBuilder(name.length)
name.forEach { c ->
if (isValidFatFilenameChar(c)) {
sb.append(c)
} else {
sb.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
}
/**
* Returns true if the given character is a valid filename character, false otherwise.
*/
private fun isValidFatFilenameChar(c: Char): Boolean {
if (0x00.toChar() <= c && c <= 0x1f.toChar()) {
return false
}
return when (c) {
'"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7f.toChar() -> false
else -> true
}
}
}
@@ -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")
@@ -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,
) )
} }
} }
@@ -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() {
@@ -0,0 +1,24 @@
package ir.armor.tachidesk.model.database.migration
import ir.armor.tachidesk.model.database.migration.lib.Migration
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
/*
* 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/. */
@Suppress("ClassName", "unused")
class M0003_DefaultCategory : Migration() {
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
override fun run() {
with(TransactionManager.current()) {
exec("ALTER TABLE CATEGORY ALTER COLUMN IS_LANDING RENAME TO IS_DEFAULT")
commit()
currentDialect.resetCaches()
}
}
}
@@ -10,7 +10,7 @@ package ir.armor.tachidesk.model.database.migration.lib
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0. // originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595 // adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
import com.google.common.reflect.ClassPath import ir.armor.tachidesk.server.ServerConfig
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@@ -18,8 +18,15 @@ import org.jetbrains.exposed.sql.SchemaUtils.create
import org.jetbrains.exposed.sql.exists import org.jetbrains.exposed.sql.exists
import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Paths
import java.time.Clock import java.time.Clock
import java.time.Instant.now import java.time.Instant.now
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.isDirectory
import kotlin.io.path.name
import kotlin.streams.toList
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -54,12 +61,31 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
logger.info { "Migrations finished successfully" } logger.info { "Migrations finished successfully" }
} }
fun loadMigrationsFrom(classPath: String): List<Migration> { @OptIn(ExperimentalPathApi::class)
return ClassPath.from(Thread.currentThread().contextClassLoader) private fun getTopLevelClasses(packageName: String): List<Class<*>> {
.getTopLevelClasses(classPath) ServerConfig::class.java.getResource("/" + "ir.armor.tachidesk.model.database.migration".replace('.', '/'))
val path = "/" + packageName.replace('.', '/')
val uri = ServerConfig::class.java.getResource(path).toURI()
return when (uri.scheme) {
"jar" -> {
val fileSystem = FileSystems.newFileSystem(uri, emptyMap<String, Any>())
fileSystem.getPath(path)
}
else -> Paths.get(uri)
}.let { Files.walk(it, 1) }
.toList()
.filterNot { it.isDirectory() || it.name.contains('$') } // '$' means it's not a top level class
.filter { it.name.endsWith(".class") }
.map { Class.forName("$packageName.${it.name.substringBefore(".class")}") }
}
@Suppress("UnstableApiUsage")
fun loadMigrationsFrom(packageName: String): List<Migration> {
return getTopLevelClasses(packageName)
.map { .map {
logger.debug("found Migration class ${it.name}") logger.debug("found Migration class ${it.name}")
val clazz = it.load().getDeclaredConstructor().newInstance() val clazz = it.getDeclaredConstructor().newInstance()
if (clazz is Migration) if (clazz is Migration)
clazz clazz
else else
@@ -13,13 +13,13 @@ import org.jetbrains.exposed.sql.ResultRow
object CategoryTable : IntIdTable() { object CategoryTable : IntIdTable() {
val name = varchar("name", 64) val name = varchar("name", 64)
val isLanding = bool("is_landing").default(false)
val order = integer("order").default(0) val order = integer("order").default(0)
val isDefault = bool("is_default").default(false)
} }
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.isDefault],
) )
@@ -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) {
@@ -11,5 +11,5 @@ data class CategoryDataClass(
val id: Int, val id: Int,
val order: Int, val order: Int,
val name: String, val name: String,
val isLanding: Boolean val default: Boolean
) )
@@ -25,7 +25,7 @@ data class ChapterDataClass(
val lastPageRead: Int, val lastPageRead: Int,
/** this chapter's index, starts with 1 */ /** this chapter's index, starts with 1 */
val index: Int? = null, val index: Int,
/** total chapter count, used to calculate if there's a next and prev chapter */ /** total chapter count, used to calculate if there's a next and prev chapter */
val chapterCount: Int? = null, val chapterCount: Int? = null,
@@ -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
@@ -14,14 +13,8 @@ import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
import ir.armor.tachidesk.impl.Chapter.getChapter import ir.armor.tachidesk.impl.Chapter.getChapter
import ir.armor.tachidesk.impl.Chapter.getChapterList import ir.armor.tachidesk.impl.Chapter.getChapterList
import ir.armor.tachidesk.impl.Chapter.modifyChapter import ir.armor.tachidesk.impl.Chapter.modifyChapter
import ir.armor.tachidesk.impl.Extension.getExtensionIcon import ir.armor.tachidesk.impl.Library
import ir.armor.tachidesk.impl.Extension.installExtension
import ir.armor.tachidesk.impl.Extension.uninstallExtension
import ir.armor.tachidesk.impl.Extension.updateExtension
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
import ir.armor.tachidesk.impl.Library.addMangaToLibrary
import ir.armor.tachidesk.impl.Library.getLibraryMangas import ir.armor.tachidesk.impl.Library.getLibraryMangas
import ir.armor.tachidesk.impl.Library.removeMangaFromLibrary
import ir.armor.tachidesk.impl.Manga.getManga import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
import ir.armor.tachidesk.impl.MangaList.getMangaList import ir.armor.tachidesk.impl.MangaList.getMangaList
@@ -34,7 +27,12 @@ import ir.armor.tachidesk.impl.Source.getSourceList
import ir.armor.tachidesk.impl.backup.BackupFlags import ir.armor.tachidesk.impl.backup.BackupFlags
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
import ir.armor.tachidesk.server.internal.About.getAbout import ir.armor.tachidesk.impl.extension.Extension.getExtensionIcon
import ir.armor.tachidesk.impl.extension.Extension.installExtension
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
import ir.armor.tachidesk.server.impl_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.Dispatchers
@@ -56,7 +54,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(SupervisorJob() + Dispatchers.IO)
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 +67,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
@@ -96,7 +95,10 @@ object JavalinSetup {
logger.error("NullPointerException while handling the request", e) logger.error("NullPointerException while handling the request", e)
ctx.status(404) ctx.status(404)
} }
app.exception(NoSuchElementException::class.java) { e, ctx ->
logger.error("NoSuchElementException while handling the request", e)
ctx.status(404)
}
app.exception(IOException::class.java) { e, ctx -> app.exception(IOException::class.java) { e, ctx ->
logger.error("IOException while handling the request", e) logger.error("IOException while handling the request", e)
ctx.status(500) ctx.status(500)
@@ -213,24 +215,6 @@ object JavalinSetup {
) )
} }
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { addMangaToLibrary(mangaId) }
)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { removeMangaFromLibrary(mangaId) }
)
}
// list manga's categories // 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()
@@ -257,7 +241,7 @@ object JavalinSetup {
app.get("/api/v1/manga/:mangaId/chapters") { ctx -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
ctx.json(future { getChapterList(mangaId, onlineFetch) }) ctx.json(future { getChapterList(mangaId, onlineFetch) })
} }
@@ -299,6 +283,16 @@ object JavalinSetup {
) )
} }
// submit a chapter for download
app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// TODO
}
// cancel a chapter download
app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// TODO
}
// global search, Not implemented yet // global search, Not implemented yet
app.get("/api/v1/search/:searchTerm") { ctx -> app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
@@ -319,6 +313,24 @@ object JavalinSetup {
ctx.json(sourceFilters(sourceId)) ctx.json(sourceFilters(sourceId))
} }
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { Library.addMangaToLibrary(mangaId) }
)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { Library.removeMangaFromLibrary(mangaId) }
)
}
// lists mangas that have no category assigned // lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx -> app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas()) ctx.json(getLibraryMangas())
@@ -345,8 +357,8 @@ object JavalinSetup {
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 isDefault = ctx.formParam("default")?.toBoolean()
updateCategory(categoryId, name, isLanding) updateCategory(categoryId, name, isDefault)
ctx.status(200) ctx.status(200)
} }
@@ -429,5 +441,19 @@ object JavalinSetup {
} }
) )
} }
// Download queue stats
app.ws("/api/v1/downloads") { ws ->
ws.onConnect { ctx ->
// TODO: send current stat
// TODO: add to downlad subscribers
}
ws.onMessage {
// TODO: send current stat
}
ws.onClose { ctx ->
// TODO: remove from subscribers
}
}
} }
} }
@@ -10,6 +10,8 @@ package ir.armor.tachidesk.server
import com.typesafe.config.Config import com.typesafe.config.Config
import io.github.config4k.getValue import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule import xyz.nulldev.ts.config.ConfigModule
import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.debugLogsEnabled
class ServerConfig(config: Config) : ConfigModule(config) { class ServerConfig(config: Config) : ConfigModule(config) {
val ip: String by config val ip: String by config
@@ -21,7 +23,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 = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by config val systemTrayEnabled: Boolean by config
val initialOpenInBrowserEnabled: Boolean by config val initialOpenInBrowserEnabled: Boolean by config
@@ -7,17 +7,15 @@ package ir.armor.tachidesk.server
* 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 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.AppMutex.handleAppMutex
import ir.armor.tachidesk.server.util.systemTray import ir.armor.tachidesk.server.util.systemTray
import mu.KotlinLogging import mu.KotlinLogging
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.bind import org.kodein.di.bind
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.singleton import org.kodein.di.singleton
import org.slf4j.Logger
import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ApplicationRootDir import xyz.nulldev.ts.config.ApplicationRootDir
@@ -37,11 +35,13 @@ class ApplicationDirs(
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
val systemTray by lazy { systemTray() } val systemTrayInstance by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() } val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() { fun applicationSetup() {
logger.info("Running Tachidesk ${BuildConfig.version} revision ${BuildConfig.revision}")
// Application dirs // Application dirs
val applicationDirs = ApplicationDirs() val applicationDirs = ApplicationDirs()
DI.global.addImport( DI.global.addImport(
@@ -65,6 +65,9 @@ fun applicationSetup() {
ServerConfig.register(GlobalConfigManager.config) ServerConfig.register(GlobalConfigManager.config)
) )
// Make sure only one instance of the app is running
handleAppMutex()
// Load config API // Load config API
DI.global.addImport(ConfigKodeinModule().create()) DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies // Load Android compatibility dependencies
@@ -72,16 +75,11 @@ fun applicationSetup() {
// start app // start app
androidCompat.startApp(App()) androidCompat.startApp(App())
// set application wide logging level
if (serverConfig.debugLogsEnabled) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
}
// create conf file if doesn't exist // create conf file if doesn't exist
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)
} }
@@ -96,8 +94,8 @@ fun applicationSetup() {
// create system tray // create system tray
if (serverConfig.systemTrayEnabled) { if (serverConfig.systemTrayEnabled) {
try { try {
systemTray systemTrayInstance
} catch (e: Exception) { } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace() e.printStackTrace()
} }
} }
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.server.internal package ir.armor.tachidesk.server.impl_internal
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -0,0 +1,18 @@
package ir.armor.tachidesk.server.util
import mu.KotlinLogging
import kotlin.system.exitProcess
private val logger = KotlinLogging.logger {}
enum class ExitCode(val code: Int) {
Success(0),
MutexCheckFailedTachideskRunning(1),
MutexCheckFailedAnotherAppRunning(2);
}
fun shutdownApp(exitCode: ExitCode) {
logger.info("Shutting Down Tachidesk. Goodbye!")
exitProcess(exitCode.code)
}
@@ -0,0 +1,77 @@
package ir.armor.tachidesk.server.util
/*
* 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 io.javalin.plugin.json.JavalinJackson
import ir.armor.tachidesk.server.impl_internal.AboutDataClass
import ir.armor.tachidesk.server.serverConfig
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.Clear
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.OtherApplicationRunning
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.TachideskInstanceRunning
import mu.KotlinLogging
import okhttp3.OkHttpClient
import okhttp3.Request.Builder
import java.io.IOException
import java.util.concurrent.TimeUnit
object AppMutex {
private val logger = KotlinLogging.logger {}
private enum class AppMutexStat(val stat: Int) {
Clear(0),
TachideskInstanceRunning(1),
OtherApplicationRunning(2)
}
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
private fun checkAppMutex(): AppMutexStat {
val client = OkHttpClient.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
.build()
val request = Builder()
.url("http://$appIP:${serverConfig.port}/api/v1/about/")
.build()
val response = try {
client.newCall(request).execute().use { response -> response.body!!.string() }
} catch (e: IOException) {
return AppMutexStat.Clear
}
return try {
JavalinJackson.fromJson(response, AboutDataClass::class.java)
AppMutexStat.TachideskInstanceRunning
} catch (e: IOException) {
AppMutexStat.OtherApplicationRunning
}
}
fun handleAppMutex() {
when (checkAppMutex()) {
Clear -> {
logger.info("Mutex status is clear, Resuming startup.")
}
TachideskInstanceRunning -> {
logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port}")
logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.")
openInBrowser()
logger.info("Aborting startup.")
shutdownApp(ExitCode.MutexCheckFailedTachideskRunning)
}
OtherApplicationRunning -> {
logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.")
shutdownApp(ExitCode.MutexCheckFailedAnotherAppRunning)
}
}
}
}
@@ -9,18 +9,18 @@ package ir.armor.tachidesk.server.util
import dorkbox.systemTray.MenuItem import dorkbox.systemTray.MenuItem
import dorkbox.systemTray.SystemTray import dorkbox.systemTray.SystemTray
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 ir.armor.tachidesk.server.util.ExitCode.Success
fun openInBrowser() { fun openInBrowser() {
val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
try { try {
Desktop.browseURL("http://127.0.0.1:4567") Desktop.browseURL("http://$appIP:${serverConfig.port}")
} catch (e: Exception) { } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace() e.printStackTrace()
} }
} }
@@ -29,8 +29,6 @@ fun systemTray(): SystemTray? {
try { try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java // ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = serverConfig.debugLogsEnabled SystemTray.DEBUG = serverConfig.debugLogsEnabled
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear(BuildConfig.name) CacheUtil.clear(BuildConfig.name)
@@ -45,16 +43,15 @@ 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") {
systemTray.shutdown() shutdownApp(Success)
exitProcess(0)
} }
) )
@@ -10,13 +10,13 @@ package ir.armor.tachidesk
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.Extension.installExtension
import ir.armor.tachidesk.impl.Extension.uninstallExtension
import ir.armor.tachidesk.impl.Extension.updateExtension
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
import ir.armor.tachidesk.impl.Source.getSourceList import ir.armor.tachidesk.impl.Source.getSourceList
import ir.armor.tachidesk.impl.extension.Extension.installExtension
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import ir.armor.tachidesk.server.applicationSetup import ir.armor.tachidesk.server.applicationSetup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
+11 -10
View File
@@ -2,19 +2,20 @@ plugins {
id("com.github.node-gradle.node") version "3.0.1" id("com.github.node-gradle.node") version "3.0.1"
} }
val nodeRoot = "${project.projectDir}/react"
node { node {
nodeProjectDir.set(file("${project.projectDir}/react/")) nodeProjectDir.set(file(nodeRoot))
} }
tasks.named("yarn_build") { tasks {
dependsOn("yarn") // install node_modules register<Copy>("copyBuild") {
} from(file("$nodeRoot/build"))
into(file("$rootDir/server/src/main/resources/react"))
tasks.register<Copy>("copyBuild") { dependsOn("yarn_build")
from(file("$rootDir/webUI/react/build")) }
into(file("$rootDir/server/src/main/resources/react"))
}
tasks.named("copyBuild") { named("yarn_build") {
dependsOn("yarn_build") dependsOn("yarn") // install node_modules
}
} }
+1 -1
View File
@@ -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';
+3 -5
View File
@@ -26,9 +26,6 @@ const useStyles = makeStyles((theme) => ({
alignItems: 'center', alignItems: 'center',
padding: 16, padding: 16,
}, },
read: {
backgroundColor: theme.palette.type === 'dark' ? '#353535' : '#f0f0f0',
},
bullet: { bullet: {
display: 'inline-block', display: 'inline-block',
margin: '0 2px', margin: '0 2px',
@@ -80,16 +77,17 @@ export default function ChapterCard(props: IProps) {
.then(() => triggerChaptersUpdate()); .then(() => triggerChaptersUpdate());
}; };
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
return ( return (
<> <>
<li> <li>
<Card> <Card>
<CardContent className={`${classes.root} ${chapter.read && classes.read}`}> <CardContent className={classes.root}>
<Link <Link
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`} to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
style={{ style={{
textDecoration: 'none', textDecoration: 'none',
color: theme.palette.text.primary, color: chapter.read ? readChapterColor : theme.palette.text.primary,
}} }}
> >
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
@@ -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);
} }
@@ -11,8 +11,11 @@ import Drawer from '@material-ui/core/Drawer';
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 CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
import ExploreIcon from '@material-ui/icons/Explore';
import ExtensionIcon from '@material-ui/icons/Extension';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import InboxIcon from '@material-ui/icons/MoveToInbox'; import SettingsIcon from '@material-ui/icons/Settings';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -47,7 +50,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library"> <ListItem button key="Library">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <CollectionsBookmarkIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Library" /> <ListItemText primary="Library" />
</ListItem> </ListItem>
@@ -55,7 +58,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions"> <ListItem button key="Extensions">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <ExtensionIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Extensions" /> <ListItemText primary="Extensions" />
</ListItem> </ListItem>
@@ -63,7 +66,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources"> <ListItem button key="Sources">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <ExploreIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Sources" /> <ListItemText primary="Sources" />
</ListItem> </ListItem>
@@ -71,7 +74,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings"> <ListItem button key="settings">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <SettingsIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Settings" /> <ListItemText primary="Settings" />
</ListItem> </ListItem>
+7
View File
@@ -1,4 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* 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 ReactDOM from 'react-dom';
import React from 'react'; import React from 'react';
import Slide, { SlideProps } from '@material-ui/core/Slide'; import Slide, { SlideProps } from '@material-ui/core/Slide';
@@ -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: {
@@ -30,8 +30,8 @@ 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...
@@ -46,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',
@@ -143,6 +143,7 @@ export const defaultReaderSettings = () => ({
staticNav: false, staticNav: false,
showPageNumber: true, showPageNumber: true,
continuesPageGap: false, continuesPageGap: false,
loadNextonEnding: false,
readerType: 'ContinuesVertical', readerType: 'ContinuesVertical',
} as IReaderSettings); } as IReaderSettings);
@@ -277,6 +278,16 @@ export default function ReaderNavBar(props: IProps) {
/> />
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </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> <ListItem>
<ListItemText primary="Reader Type" /> <ListItemText primary="Reader Type" />
<Select <Select
+11 -7
View File
@@ -60,11 +60,13 @@ function LazyImage(props: IProps) {
}; };
useEffect(() => { useEffect(() => {
window.addEventListener('scroll', handleScroll); if (settings.readerType === 'Webtoon' || settings.readerType === 'ContinuesVertical') {
window.addEventListener('scroll', handleScroll);
return () => { return () => {
window.removeEventListener('scroll', handleScroll); window.removeEventListener('scroll', handleScroll);
}; };
} return () => {};
}, [handleScroll]); }, [handleScroll]);
useEffect(() => { useEffect(() => {
@@ -92,14 +94,14 @@ function LazyImage(props: IProps) {
); );
} }
export default function Page(props: IProps) { const Page = React.forwardRef((props: IProps, ref: any) => {
const { const {
src, index, setCurPage, settings, src, index, setCurPage, settings,
} = props; } = props;
const classes = useStyles(settings)(); const classes = useStyles(settings)();
return ( return (
<div style={{ margin: '0 auto' }}> <div ref={ref} style={{ margin: '0 auto' }}>
<LazyImage <LazyImage
src={src} src={src}
index={index} index={index}
@@ -108,4 +110,6 @@ export default function Page(props: IProps) {
/> />
</div> </div>
); );
} });
export default Page;
@@ -1,3 +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/. */
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import React from 'react'; import React from 'react';
@@ -1,36 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Page from './Page';
const useStyles = makeStyles({
reader: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
width: '100%',
},
});
export default function VerticalReader(props: IReaderProps) {
const { pages, settings, setCurPage } = props;
const classes = useStyles();
return (
<div className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
setCurPage={setCurPage}
settings={settings}
/>
))
}
</div>
);
}
@@ -1,7 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* 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 { makeStyles } from '@material-ui/core/styles';
import React from 'react'; import React from 'react';
import Page from './Page'; import Page from '../Page';
const useStyles = makeStyles({ const useStyles = makeStyles({
reader: { reader: {
@@ -21,7 +28,7 @@ interface IProps {
settings: IReaderSettings settings: IReaderSettings
} }
export default function HorizontalReader(props: IProps) { export default function HorizontalPager(props: IProps) {
const { pages, settings, setCurPage } = props; const { pages, settings, setCurPage } = props;
const classes = useStyles(); const classes = useStyles();
@@ -1,7 +1,15 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* 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 { makeStyles } from '@material-ui/core/styles';
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import Page from './Page'; import { useHistory } from 'react-router-dom';
import Page from '../Page';
const useStyles = makeStyles({ const useStyles = makeStyles({
reader: { reader: {
@@ -16,13 +24,20 @@ const useStyles = makeStyles({
export default function PagedReader(props: IReaderProps) { export default function PagedReader(props: IReaderProps) {
const { const {
pages, settings, setCurPage, curPage, pages, settings, setCurPage, curPage, manga, chapter, nextChapter,
} = props; } = props;
const classes = useStyles(); const classes = useStyles();
const history = useHistory();
const pageRef = useRef<HTMLDivElement>(null);
function nextPage() { function nextPage() {
if (curPage < pages.length - 1) { setCurPage(curPage + 1); } if (curPage < pages.length - 1) {
setCurPage(curPage + 1);
} else if (settings.loadNextonEnding) {
nextChapter();
}
} }
function prevPage() { function prevPage() {
@@ -52,16 +67,16 @@ export default function PagedReader(props: IReaderProps) {
useEffect(() => { useEffect(() => {
document.addEventListener('keyup', keyboardControl, false); document.addEventListener('keyup', keyboardControl, false);
document.addEventListener('click', clickControl); pageRef.current?.addEventListener('click', clickControl);
return () => { return () => {
document.removeEventListener('keyup', keyboardControl); document.removeEventListener('keyup', keyboardControl);
document.removeEventListener('click', clickControl); pageRef.current?.removeEventListener('click', clickControl);
}; };
}, [curPage]); }, [curPage, pageRef]);
return ( return (
<div className={classes.reader}> <div ref={pageRef} className={classes.reader}>
<Page <Page
key={curPage} key={curPage}
index={curPage} index={curPage}
@@ -0,0 +1,76 @@
/* 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, useState } 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, nextChapter,
} = props;
const classes = useStyles();
const history = useHistory();
const [initialScroll, setInitialScroll] = useState(-1);
const initialPageRef = useRef<HTMLDivElement>(null);
const handleLoadNextonEnding = () => {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
nextChapter();
}
};
useEffect(() => {
if (settings.loadNextonEnding) { window.addEventListener('scroll', handleLoadNextonEnding); }
return () => {
window.removeEventListener('scroll', handleLoadNextonEnding);
};
}, []);
useEffect(() => {
if ((chapter as IChapter).lastPageRead > -1) {
setInitialScroll((chapter as IChapter).lastPageRead);
}
}, []);
useEffect(() => {
if (initialScroll > -1) {
initialPageRef.current?.scrollIntoView();
}
}, [initialScroll, initialPageRef.current]);
return (
<div className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
setCurPage={setCurPage}
settings={settings}
ref={page.index === initialScroll ? initialPageRef : null}
/>
))
}
</div>
);
}
+3 -2
View File
@@ -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
@@ -71,7 +72,7 @@ export default function Library() {
const defaultCategoryTab = { const defaultCategoryTab = {
category: { category: {
name: 'Default', name: 'Default',
isLanding: true, default: true,
order: 0, order: 0,
id: -1, id: -1,
}, },
@@ -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;
+9 -14
View File
@@ -44,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();
@@ -60,6 +56,7 @@ 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 [fetchedChapters, setFetchedChapters] = useState(false);
const [noChaptersFound, setNoChaptersFound] = useState(false);
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0); const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
function triggerChaptersUpdate() { function triggerChaptersUpdate() {
@@ -84,20 +81,13 @@ export default function Manga() {
.then((data) => { .then((data) => {
if (data.length === 0 && fetchedChapters) { if (data.length === 0 && fetchedChapters) {
makeToast('No chapters found', 'warning'); makeToast('No chapters found', 'warning');
setNoChaptersFound(true);
} }
setChapters(data); setChapters(data);
}) })
.then(() => setFetchedChapters(true)); .then(() => setFetchedChapters(true));
}, [chapters.length, fetchedChapters, chapterUpdateTriggerer]); }, [chapters.length, fetchedChapters, chapterUpdateTriggerer]);
// const itemContent = (index:any) => <InnerItem chapters={chapters} index={index} />;
const itemContent = (index:any) => (
<ChapterCard
chapter={chapters[index]}
triggerChaptersUpdate={triggerChaptersUpdate}
/>
);
return ( return (
<div className={classes.root}> <div className={classes.root}>
<LoadingPlaceholder <LoadingPlaceholder
@@ -107,7 +97,7 @@ export default function Manga() {
/> />
<LoadingPlaceholder <LoadingPlaceholder
shouldRender={chapters.length > 0 || fetchedChapters} 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
@@ -115,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}
/> />
+57 -13
View File
@@ -9,16 +9,17 @@
import CircularProgress from '@material-ui/core/CircularProgress'; 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 { useHistory, useParams } from 'react-router-dom';
import HorizontalReader from '../components/reader/HorizontalReader'; import HorizontalPager from '../components/reader/pager/HorizontalPager';
import Page from '../components/reader/Page'; import Page from '../components/reader/Page';
import PageNumber from '../components/reader/PageNumber'; import PageNumber from '../components/reader/PageNumber';
import PagedReader from '../components/reader/PagedReader'; import WebtoonPager from '../components/reader/pager/PagedPager';
import VerticalReader from '../components/reader/VerticalReader'; import VerticalPager from '../components/reader/pager/VerticalPager';
import ReaderNavBar, { defaultReaderSettings } from '../components/ReaderNavBar'; 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({
root: { root: {
@@ -33,24 +34,24 @@ const useStyles = (settings: IReaderSettings) => makeStyles({
const getReaderComponent = (readerType: ReaderType) => { const getReaderComponent = (readerType: ReaderType) => {
switch (readerType) { switch (readerType) {
case 'ContinuesVertical': case 'ContinuesVertical':
return VerticalReader; return VerticalPager;
break; break;
case 'Webtoon': case 'Webtoon':
return VerticalReader; return VerticalPager;
break; break;
case 'SingleVertical': case 'SingleVertical':
return PagedReader; return WebtoonPager;
break; break;
case 'SingleRTL': case 'SingleRTL':
return PagedReader; return WebtoonPager;
break; break;
case 'SingleLTR': case 'SingleLTR':
return PagedReader; return WebtoonPager;
break; break;
case 'ContinuesHorizontal': case 'ContinuesHorizontal':
return HorizontalReader; return HorizontalPager;
default: default:
return VerticalReader; return VerticalPager;
break; break;
} }
}; };
@@ -62,6 +63,7 @@ export default function Reader() {
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings); const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
const classes = useStyles(settings)(); const classes = useStyles(settings)();
const history = useHistory();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', ''); const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
@@ -69,9 +71,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,
@@ -107,9 +122,24 @@ export default function Reader() {
.then((response) => response.data) .then((response) => response.data)
.then((data:IChapter) => { .then((data:IChapter) => {
setChapter(data); setChapter(data);
setCurPage(data.lastPageRead);
}); });
}, [chapterIndex]); }, [chapterIndex]);
useEffect(() => {
if (curPage !== -1) {
const formData = new FormData();
formData.append('lastPageRead', curPage.toString());
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
}
if (curPage === chapter.pageCount - 1) {
const formDataRead = new FormData();
formDataRead.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formDataRead);
}
}, [curPage]);
if (chapter.pageCount === -1) { if (chapter.pageCount === -1) {
return ( return (
<div className={classes.loading}> <div className={classes.loading}>
@@ -118,6 +148,17 @@ export default function Reader() {
); );
} }
const nextChapter = () => {
if (chapter.index < chapter.chapterCount) {
const formData = new FormData();
formData.append('lastPageRead', `${chapter.pageCount - 1}`);
formData.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
}
};
const pages = range(chapter.pageCount).map((index) => ({ const pages = range(chapter.pageCount).map((index) => ({
index, index,
src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`, src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`,
@@ -138,6 +179,9 @@ export default function Reader() {
setCurPage={setCurPage} setCurPage={setCurPage}
curPage={curPage} curPage={curPage}
settings={settings} settings={settings}
manga={manga}
chapter={chapter}
nextChapter={nextChapter}
/> />
</div> </div>
); );
+4 -3
View File
@@ -7,7 +7,8 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import InboxIcon from '@material-ui/icons/Inbox'; import ListAltIcon from '@material-ui/icons/ListAlt';
import BackupIcon from '@material-ui/icons/Backup';
import Brightness6Icon from '@material-ui/icons/Brightness6'; import Brightness6Icon from '@material-ui/icons/Brightness6';
import DnsIcon from '@material-ui/icons/Dns'; import DnsIcon from '@material-ui/icons/Dns';
import EditIcon from '@material-ui/icons/Edit'; import EditIcon from '@material-ui/icons/Edit';
@@ -50,13 +51,13 @@ export default function Settings() {
<List style={{ padding: 0 }}> <List style={{ padding: 0 }}>
<ListItemLink href="/settings/categories"> <ListItemLink href="/settings/categories">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <ListAltIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Categories" /> <ListItemText primary="Categories" />
</ListItemLink> </ListItemLink>
<ListItemLink href="/settings/backup"> <ListItemLink href="/settings/backup">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <BackupIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Backup" /> <ListItemText primary="Backup" />
</ListItemLink> </ListItemLink>
+29 -12
View File
@@ -28,8 +28,9 @@ import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import NavbarContext from '../../context/NavbarContext'; import NavbarContext from '../../context/NavbarContext';
import client from '../../util/client'; import client from '../../util/client';
@@ -49,7 +50,8 @@ export default function Categories() {
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState(''); const [dialogName, setDialogName] = useState('');
const [dialogDefault, setDialogDefault] = useState(false);
const theme = useTheme(); const theme = useTheme();
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
@@ -93,7 +95,8 @@ export default function Categories() {
}; };
const resetDialog = () => { const resetDialog = () => {
setDialogValue(''); setDialogName('');
setDialogDefault(false);
setCategoryToEdit(-1); setCategoryToEdit(-1);
}; };
@@ -102,6 +105,13 @@ export default function Categories() {
setDialogOpen(true); setDialogOpen(true);
}; };
const handleEditDialogOpen = (index) => {
setDialogName(categories[index].name);
setDialogDefault(categories[index].default);
setCategoryToEdit(index);
setDialogOpen(true);
};
const handleDialogCancel = () => { const handleDialogCancel = () => {
setDialogOpen(false); setDialogOpen(false);
}; };
@@ -110,7 +120,8 @@ export default function Categories() {
setDialogOpen(false); setDialogOpen(false);
const formData = new FormData(); const formData = new FormData();
formData.append('name', dialogValue); formData.append('name', dialogName);
formData.append('default', dialogDefault);
if (categoryToEdit === -1) { if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData) client.post('/api/v1/category/', formData)
@@ -161,8 +172,7 @@ export default function Categories() {
/> />
<IconButton <IconButton
onClick={() => { onClick={() => {
handleDialogOpen(); handleEditDialogOpen(index);
setCategoryToEdit(index);
}} }}
> >
<EditIcon /> <EditIcon />
@@ -197,12 +207,9 @@ export default function Categories() {
</Fab> </Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel}> <Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogTitle id="form-dialog-title"> <DialogTitle id="form-dialog-title">
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`} {categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>
Enter new category name.
</DialogContentText>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
@@ -210,8 +217,18 @@ export default function Categories() {
label="Category Name" label="Category Name"
type="text" type="text"
fullWidth fullWidth
value={dialogValue} value={dialogName}
onChange={(e) => setDialogValue(e.target.value)} onChange={(e) => setDialogName(e.target.value)}
/>
<FormControlLabel
control={(
<Checkbox
checked={dialogDefault}
onChange={(e) => setDialogDefault(e.target.checked)}
color="default"
/>
)}
label="Default category when adding new manga to library"
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
+5 -1
View File
@@ -80,7 +80,7 @@ interface ICategory {
id: number id: number
order: number order: number
name: String name: String
isLanding: boolean default: boolean
} }
interface INavbarOverride { interface INavbarOverride {
@@ -99,6 +99,7 @@ type ReaderType =
interface IReaderSettings{ interface IReaderSettings{
staticNav: boolean staticNav: boolean
showPageNumber: boolean showPageNumber: boolean
loadNextonEnding: boolean
readerType: ReaderType readerType: ReaderType
} }
@@ -113,4 +114,7 @@ interface IReaderProps {
setCurPage: React.Dispatch<React.SetStateAction<number>> setCurPage: React.Dispatch<React.SetStateAction<number>>
curPage: number curPage: number
settings: IReaderSettings settings: IReaderSettings
manga: IMangaCard | IManga
chapter: IChapter | IPartialChpter
nextChapter: () => void
} }
+10
View File
@@ -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;
}