Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e0d08ae5a | |||
| 986b4c2c27 | |||
| 0bf9ccfcbd | |||
| 5e8c47928d | |||
| ffae7f911f | |||
| e37fdf6d79 | |||
| b359116745 | |||
| 60073aace3 | |||
| 874b13fa14 | |||
| b146d1024b | |||
| 332e95c021 | |||
| 1f68141df5 | |||
| dd731cd306 | |||
| 38d8d03cae | |||
| ec7d840f37 | |||
| 2813dbb897 | |||
| 77d1402b8a | |||
| 08e8a9d105 | |||
| 71661f70b6 | |||
| ac1e79ba83 | |||
| d082809776 | |||
| a458a696db | |||
| 75786a91b0 | |||
| 6ddb5db57b | |||
| 4f70cc9283 | |||
| 23b643d637 | |||
| fdfc256c4d | |||
| fba56c1b75 | |||
| 4743bfacf7 | |||
| 2356537f7c | |||
| fa071aee84 | |||
| c00ca23a8b | |||
| 733b017936 | |||
| 4147f2e368 | |||
| 154b9992eb | |||
| 88b881b043 | |||
| 5d1491fb8c | |||
| 3a33196cf1 | |||
| fa8e0478da | |||
| 7e7e069244 | |||
| 18e0d34af0 | |||
| 3fe3f35483 | |||
| cf8e274883 | |||
| 10dee8b345 | |||
| ae8d30593f | |||
| 9cde46b5da | |||
| 8e61632155 | |||
| e2c4b4cb57 | |||
| 326da504ea | |||
| c5874a3f10 | |||
| 02802fab97 | |||
| 29dea10be2 | |||
| 6bc36193dc | |||
| 81e123388e | |||
| 8ebd7869a5 | |||
| 7a2f5f13f1 | |||
| 25d7dad39f | |||
| c8f8795920 | |||
| 84206a7074 | |||
| 6fd8b36dca | |||
| d1500baae1 | |||
| 045801dd1a | |||
| 14a2cbc793 | |||
| fd385017df | |||
| 9b05954cf2 | |||
| 6aaf636069 | |||
| d30e89e5ec | |||
| 7acc745478 | |||
| 5a9a2d816e | |||
| 105f11ed02 | |||
| 3021437a05 | |||
| 439602fc03 | |||
| 34e13b9589 | |||
| 2aab4ae918 | |||
| 7ef67671a4 | |||
| e8df84416c | |||
| be930bb68b | |||
| db52948865 | |||
| d2a72526f6 | |||
| 0a9f57b32b | |||
| 180f210536 | |||
| c1baa31eed | |||
| cacc97cec7 | |||
| d5691fd81c | |||
| 49dc9fe5f6 | |||
| c0b49c7428 | |||
| fa345af42d | |||
| db3cc786a1 | |||
| fe879ae51d |
+3
-2
@@ -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
|
||||||
@@ -24,4 +24,5 @@
|
|||||||
*.woff binary
|
*.woff binary
|
||||||
*.pyc binary
|
*.pyc binary
|
||||||
*.swp binary
|
*.swp binary
|
||||||
*.pdf binary
|
*.pdf binary
|
||||||
|
*.exe binary
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cp master/server/build/Tachidesk-*.jar preview
|
|
||||||
cd preview
|
|
||||||
|
|
||||||
new_jar_build=$(ls *.jar| tail -1)
|
|
||||||
echo "last jar build file name: $new_jar_build"
|
|
||||||
|
|
||||||
cp -f $new_jar_build Tachidesk-latest.jar
|
|
||||||
|
|
||||||
rm -rf latest_pointer/*
|
|
||||||
cp $new_jar_build latest_pointer
|
|
||||||
cp ../master/server/build/Tachidesk-*.zip latest_pointer
|
|
||||||
|
|
||||||
latest=$(ls *.jar | tail -n1 | sed -e's/Tachidesk-\|.jar//g')
|
|
||||||
echo "{ \"latest\": \"$latest\" }" > index.json
|
|
||||||
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git status
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
git add .
|
|
||||||
git commit -m "Update preview repository"
|
|
||||||
git push
|
|
||||||
else
|
|
||||||
echo "No changes to commit"
|
|
||||||
fi
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -69,17 +69,59 @@ jobs:
|
|||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
- name: make windows package
|
# - name: Mock Build and copy webUI, Build Jar
|
||||||
|
# run: |
|
||||||
|
# mkdir -p master/server/build
|
||||||
|
# cd master/server/build
|
||||||
|
# echo "test" > Tachidesk-v0.3.8-r583.jar
|
||||||
|
|
||||||
|
- name: Generate Tag Name
|
||||||
|
id: GenTagName
|
||||||
|
run: |
|
||||||
|
cd master/server/build
|
||||||
|
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
|
||||||
|
echo "$genTag"
|
||||||
|
echo "::set-output name=value::$genTag"
|
||||||
|
|
||||||
|
- name: make windows packages
|
||||||
run: |
|
run: |
|
||||||
cd master/scripts
|
cd master/scripts
|
||||||
./windows-bundler.sh
|
./windows-bundler.sh win32
|
||||||
|
./windows-bundler.sh win64
|
||||||
|
|
||||||
|
# - name: Mock make windows packages
|
||||||
|
# run: |
|
||||||
|
# cd master/server/build
|
||||||
|
# echo test > Tachidesk-v0.3.8-r580-win32.zip
|
||||||
|
|
||||||
- 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: Create Tag
|
||||||
run: |
|
run: |
|
||||||
./master/.github/scripts/commit-preview.sh
|
TAG="${{ steps.GenTagName.outputs.value }}"
|
||||||
|
echo "tag: $TAG"
|
||||||
|
cd preview
|
||||||
|
echo "{ \"latest\": \"$TAG\" }" > index.json
|
||||||
|
git add index.json
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git commit -m "Updated to $TAG"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
|
||||||
|
- name: Upload Preview Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
|
||||||
|
owner: "Suwayomi"
|
||||||
|
repo: "Tachidesk-preview"
|
||||||
|
tag: ${{ steps.GenTagName.outputs.value }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -68,13 +68,14 @@ jobs:
|
|||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
- name: make windows package
|
- name: make windows packages
|
||||||
run: |
|
run: |
|
||||||
cd master/scripts
|
cd master/scripts
|
||||||
./windows-bundler.sh
|
./windows-bundler.sh win32
|
||||||
|
./windows-bundler.sh win64
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@master
|
uses: xresloader/upload-to-github-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
+6
-1
@@ -8,4 +8,9 @@ build
|
|||||||
|
|
||||||
server/src/main/resources/react
|
server/src/main/resources/react
|
||||||
server/tmp/
|
server/tmp/
|
||||||
server/tachiserver-data/
|
server/tachiserver-data/
|
||||||
|
|
||||||
|
# bundle asset downlaods
|
||||||
|
OpenJDK*.zip
|
||||||
|
electron-*.zip
|
||||||
|
rcedit-*
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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..."
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
| Build | Stable | Preview | Support Server |
|
| Build | Stable | Preview | Support Server |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](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:
|
||||||
|
|
||||||
@@ -30,24 +32,24 @@ Here is a list of current features:
|
|||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
|
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||||
|
|
||||||
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
|
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||||
|
|
||||||
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 "Stable" win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/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 one of the Launcher files depending on what you want(see bellow). The rest works like the previous section.
|
||||||
|
#### Windows Launchers
|
||||||
|
- `Tachidesk Electron Launcher.bat`: Launches Tachidesk inside Electron as a desktop applicaton
|
||||||
|
- `Tachidesk Browser Launcher.bat`: Launches Tachidesk in a browser window
|
||||||
|
- `Tachidesk Debug Launcher.bat`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
|
||||||
|
|
||||||
### 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
@@ -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 @@
|
|||||||
|
start "" jre/bin/javaw -jar Tachidesk.jar
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\javaw "-Dir.armor.tachidesk.webInterface=electron" "-Dir.armor.tachidesk.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
system("start jre\\bin\\javaw -jar Tachidesk.jar");
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
start "" "jre/bin/javaw -jar Tachidesk.jar"
|
|
||||||
@@ -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`
|
||||||
+57
-10
@@ -6,31 +6,78 @@
|
|||||||
# 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/.
|
||||||
|
|
||||||
echo "Downloading jre..."
|
electron_version="v12.0.9"
|
||||||
|
|
||||||
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
if [ $1 = "win32" ]; then
|
||||||
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
|
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
arch="win32"
|
||||||
|
electron="electron-$electron_version-win32-ia32.zip"
|
||||||
|
else
|
||||||
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
arch="win64"
|
||||||
|
electron="electron-$electron_version-win32-x64.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jre_dir="jdk8u292-b10-jre"
|
||||||
|
|
||||||
echo "creating windows bundle"
|
echo "creating windows bundle"
|
||||||
|
|
||||||
jar=$(ls ../server/build/Tachidesk-*.jar)
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
jar_name=$(echo $jar | cut -d'/' -f4)
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-$arch
|
||||||
|
|
||||||
|
|
||||||
# make release dir
|
# make release dir
|
||||||
mkdir $release_name
|
mkdir $release_name
|
||||||
|
|
||||||
|
|
||||||
|
echo "Dealing with jre..."
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
|
||||||
|
fi
|
||||||
unzip $jre
|
unzip $jre
|
||||||
|
mv $jre_dir $release_name/jre
|
||||||
|
|
||||||
# move jre
|
echo "Dealing with electron"
|
||||||
mv jdk8u292-b10-jre $release_name/jre
|
if [ ! -f $electron ]; then
|
||||||
|
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
|
||||||
|
fi
|
||||||
|
unzip $electron -d $release_name/electron
|
||||||
|
|
||||||
|
# change electron's icon
|
||||||
|
rcedit="rcedit-x86.exe"
|
||||||
|
if [ ! -f $rcedit ]; then
|
||||||
|
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check if running under github actions
|
||||||
|
if [ $CI = true ]; then
|
||||||
|
# change electron executable's icon
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
|
||||||
|
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
|
||||||
|
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
|
||||||
|
sudo apt install --install-recommends winehq-stable
|
||||||
|
fi
|
||||||
|
# this script assumes that wine is installed here on out
|
||||||
|
|
||||||
|
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
|
||||||
|
|
||||||
|
# copy artifacts
|
||||||
cp $jar $release_name/Tachidesk.jar
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
cp resources/Tachidesk.bat $release_name
|
cp "resources/Tachidesk Browser Launcher.bat" $release_name
|
||||||
cp resources/Tachidesk-debug.bat $release_name
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Electron Launcher.bat" $release_name
|
||||||
|
|
||||||
zip_name=$release_name.zip
|
zip_name=$release_name.zip
|
||||||
zip -9 -r $zip_name $release_name
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
cp $zip_name ../server/build/
|
rm -rf $release_name
|
||||||
|
|
||||||
|
# clean up from possible previous runs
|
||||||
|
if [ -f ../server/build/$zip_name ]; then
|
||||||
|
rm ../server/build/$zip_name
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
|
|||||||
+35
-54
@@ -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"))
|
||||||
@@ -86,6 +75,12 @@ dependencies {
|
|||||||
val MainClass = "ir.armor.tachidesk.MainKt"
|
val MainClass = "ir.armor.tachidesk.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
|
// for testing electron
|
||||||
|
// applicationDefaultJvmArgs = listOf(
|
||||||
|
// "-Dir.armor.tachidesk.webInterface=electron",
|
||||||
|
// "-Dir.armor.tachidesk.electronPath=/home/armor/programming/Suwayomi/Tachidesk/scripts/electron-v12.0.9-linux-x64/electron"
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -97,12 +92,12 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.3.4"
|
val tachideskVersion = "v0.3.9"
|
||||||
|
|
||||||
// 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 +121,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-win64"
|
|
||||||
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 +134,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,11 +147,11 @@ tasks {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnit()
|
useJUnit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
withType<ShadowJar> {
|
withType<ShadowJar> {
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
@@ -185,11 +167,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
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-7
@@ -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"
|
||||||
|
|
||||||
+6
-6
@@ -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()
|
||||||
+119
@@ -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
-1
@@ -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
|
||||||
+16
-23
@@ -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,18 @@ 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? {
|
||||||
val target = "$fileName."
|
val target = "$fileName."
|
||||||
File(directoryPath).listFiles().forEach { file ->
|
File(directoryPath).listFiles().orEmpty().forEach { file ->
|
||||||
if (file.name.startsWith(target))
|
if (file.name.startsWith(target))
|
||||||
return "$directoryPath/${file.name}"
|
return "$directoryPath/${file.name}"
|
||||||
}
|
}
|
||||||
@@ -46,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-5
@@ -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,13 +61,31 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
|||||||
logger.info { "Migrations finished successfully" }
|
logger.info { "Migrations finished successfully" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPathApi::class)
|
||||||
|
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
|
||||||
|
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")
|
@Suppress("UnstableApiUsage")
|
||||||
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
fun loadMigrationsFrom(packageName: String): List<Migration> {
|
||||||
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
return getTopLevelClasses(packageName)
|
||||||
.getTopLevelClasses(classPath)
|
|
||||||
.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[this.id].value,
|
categoryEntry[this.id].value,
|
||||||
categoryEntry[this.order],
|
categoryEntry[this.order],
|
||||||
categoryEntry[this.name],
|
categoryEntry[this.name],
|
||||||
categoryEntry[this.isLanding],
|
categoryEntry[this.isDefault],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -13,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
|
||||||
@@ -33,17 +27,22 @@ 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.server.util.openInBrowser
|
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.Browser
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.future.future
|
import kotlinx.coroutines.future.future
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -56,7 +55,7 @@ import kotlin.concurrent.thread
|
|||||||
object JavalinSetup {
|
object JavalinSetup {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Executors.newFixedThreadPool(200).asCoroutineDispatcher())
|
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)
|
||||||
@@ -79,6 +78,12 @@ object JavalinSetup {
|
|||||||
hasWebUiBundled = false
|
hasWebUiBundled = false
|
||||||
}
|
}
|
||||||
config.enableCorsForAllOrigins()
|
config.enableCorsForAllOrigins()
|
||||||
|
}.events { event ->
|
||||||
|
event.serverStarted {
|
||||||
|
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||||
|
Browser.openInBrowser()
|
||||||
|
}
|
||||||
|
}
|
||||||
}.start(serverConfig.ip, serverConfig.port)
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
|
|
||||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||||
@@ -88,15 +93,14 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
|
||||||
openInBrowser()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
app.exception(NullPointerException::class.java) { e, ctx ->
|
||||||
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 +217,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 +243,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 +285,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 +315,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 +359,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 +443,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 = System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("debugLogsEnabled")).toBoolean()
|
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,16 +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.model.database.databaseUp
|
import ir.armor.tachidesk.model.database.databaseUp
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
import ir.armor.tachidesk.server.util.AppMutex.handleAppMutex
|
||||||
|
import ir.armor.tachidesk.server.util.SystemTray.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
|
||||||
@@ -36,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(
|
||||||
@@ -64,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
|
||||||
@@ -71,11 +75,6 @@ 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")
|
||||||
@@ -95,7 +94,7 @@ fun applicationSetup() {
|
|||||||
// create system tray
|
// create system tray
|
||||||
if (serverConfig.systemTrayEnabled) {
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTrayInstance
|
||||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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,78 @@
|
|||||||
|
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 ir.armor.tachidesk.server.util.Browser.openInBrowser
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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 dorkbox.util.Desktop
|
||||||
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
|
|
||||||
|
object Browser {
|
||||||
|
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||||
|
private val appBaseUrl = "http://$appIP:${serverConfig.port}"
|
||||||
|
|
||||||
|
private val electronInstances = mutableListOf<Any>()
|
||||||
|
|
||||||
|
fun openInBrowser() {
|
||||||
|
|
||||||
|
val openInElectron = System.getProperty("ir.armor.tachidesk.webInterface")?.equals("electron")
|
||||||
|
|
||||||
|
if (openInElectron == true) {
|
||||||
|
try {
|
||||||
|
val electronPath = System.getProperty("ir.armor.tachidesk.electronPath")!!
|
||||||
|
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||||
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Desktop.browseURL(appBaseUrl)
|
||||||
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,60 +9,50 @@ 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 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 ir.armor.tachidesk.server.serverConfig
|
||||||
import kotlin.system.exitProcess
|
import ir.armor.tachidesk.server.util.Browser.openInBrowser
|
||||||
|
import ir.armor.tachidesk.server.util.ExitCode.Success
|
||||||
|
|
||||||
fun openInBrowser() {
|
object SystemTray {
|
||||||
try {
|
fun systemTray(): SystemTray? {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
try {
|
||||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||||
e.printStackTrace()
|
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
||||||
}
|
|
||||||
}
|
CacheUtil.clear(BuildConfig.name)
|
||||||
|
|
||||||
fun systemTray(): SystemTray? {
|
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
|
||||||
try {
|
val mainMenu = systemTray.menu
|
||||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
|
||||||
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
mainMenu.add(
|
||||||
if (System.getProperty("os.name").startsWith("Windows"))
|
MenuItem(
|
||||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
"Open Tachidesk"
|
||||||
|
) {
|
||||||
CacheUtil.clear(BuildConfig.name)
|
openInBrowser()
|
||||||
|
}
|
||||||
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
|
)
|
||||||
val mainMenu = systemTray.menu
|
|
||||||
|
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
||||||
mainMenu.add(
|
|
||||||
MenuItem(
|
// systemTray.setTooltip("Tachidesk")
|
||||||
"Open Tachidesk"
|
systemTray.setImage(icon)
|
||||||
) {
|
// systemTray.status = "No Mail"
|
||||||
openInBrowser()
|
|
||||||
}
|
mainMenu.add(
|
||||||
)
|
MenuItem("Quit") {
|
||||||
|
shutdownApp(Success)
|
||||||
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
}
|
||||||
|
)
|
||||||
// systemTray.setTooltip("Tachidesk")
|
|
||||||
systemTray.setImage(icon)
|
systemTray.installShutdownHook()
|
||||||
// systemTray.status = "No Mail"
|
|
||||||
|
return systemTray
|
||||||
mainMenu.add(
|
} catch (e: Exception) {
|
||||||
MenuItem("Quit") {
|
e.printStackTrace()
|
||||||
systemTray.shutdown()
|
return null
|
||||||
exitProcess(0)
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
systemTray.installShutdownHook()
|
|
||||||
|
|
||||||
return systemTray
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+12
-11
@@ -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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
export default function PagedReader(props: IReaderProps) {
|
export default function PagedReader(props: IReaderProps) {
|
||||||
const {
|
const {
|
||||||
pages, settings, setCurPage, curPage, manga, chapter,
|
pages, settings, setCurPage, curPage, manga, chapter, nextChapter,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
@@ -36,7 +36,7 @@ export default function PagedReader(props: IReaderProps) {
|
|||||||
if (curPage < pages.length - 1) {
|
if (curPage < pages.length - 1) {
|
||||||
setCurPage(curPage + 1);
|
setCurPage(curPage + 1);
|
||||||
} else if (settings.loadNextonEnding) {
|
} else if (settings.loadNextonEnding) {
|
||||||
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
nextChapter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* 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 { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import Page from '../Page';
|
import Page from '../Page';
|
||||||
|
|
||||||
@@ -23,16 +23,18 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
export default function VerticalReader(props: IReaderProps) {
|
export default function VerticalReader(props: IReaderProps) {
|
||||||
const {
|
const {
|
||||||
pages, settings, setCurPage, curPage, manga, chapter,
|
pages, settings, setCurPage, curPage, manga, chapter, nextChapter,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [initialScroll, setInitialScroll] = useState(-1);
|
||||||
|
const initialPageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleLoadNextonEnding = () => {
|
const handleLoadNextonEnding = () => {
|
||||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
setCurPage(0);
|
nextChapter();
|
||||||
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -43,6 +45,18 @@ export default function VerticalReader(props: IReaderProps) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((chapter as IChapter).lastPageRead > -1) {
|
||||||
|
setInitialScroll((chapter as IChapter).lastPageRead);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialScroll > -1) {
|
||||||
|
initialPageRef.current?.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [initialScroll, initialPageRef.current]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.reader}>
|
<div className={classes.reader}>
|
||||||
{
|
{
|
||||||
@@ -53,6 +67,7 @@ export default function VerticalReader(props: IReaderProps) {
|
|||||||
src={page.src}
|
src={page.src}
|
||||||
setCurPage={setCurPage}
|
setCurPage={setCurPage}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
|
ref={page.index === initialScroll ? initialPageRef : null}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
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 HorizontalPager from '../components/reader/pager/HorizontalPager';
|
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';
|
||||||
@@ -63,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', '');
|
||||||
|
|
||||||
@@ -117,14 +118,28 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChapter(initialChapter);
|
setChapter(initialChapter);
|
||||||
setCurPage(0);
|
|
||||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data:IChapter) => {
|
||||||
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}>
|
||||||
@@ -133,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}`,
|
||||||
@@ -155,6 +181,7 @@ export default function Reader() {
|
|||||||
settings={settings}
|
settings={settings}
|
||||||
manga={manga}
|
manga={manga}
|
||||||
chapter={chapter}
|
chapter={chapter}
|
||||||
|
nextChapter={nextChapter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Vendored
+2
-1
@@ -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 {
|
||||||
@@ -116,4 +116,5 @@ interface IReaderProps {
|
|||||||
settings: IReaderSettings
|
settings: IReaderSettings
|
||||||
manga: IMangaCard | IManga
|
manga: IMangaCard | IManga
|
||||||
chapter: IChapter | IPartialChpter
|
chapter: IChapter | IPartialChpter
|
||||||
|
nextChapter: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user