Compare commits
309 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c6d043277 | |||
| 9a226f7b64 | |||
| 3b73a0fd72 | |||
| 2478aa77cd | |||
| a41068dbc9 | |||
| 5e47b7ae6b | |||
| debf45a7d5 | |||
| e7041e8c8c | |||
| bd960992bc | |||
| 0c5f6b432c | |||
| 49232edbd5 | |||
| b02884f58d | |||
| 845b588426 | |||
| 3a97d7c8be | |||
| 2cb2ded2d9 | |||
| 14e02bee6c | |||
| 30f7cdc1ba | |||
| 420d14fc37 | |||
| 3d7953d977 | |||
| 35238b3da1 | |||
| 446f4283e0 | |||
| 8062dd3709 | |||
| 41c8fde8c5 | |||
| d90b986d19 | |||
| 64ea8416b2 | |||
| 100a4c9d35 | |||
| 4ef6dec89a | |||
| a14cdc48bd | |||
| 8a4ddbc6df | |||
| ee33acc561 | |||
| 5fe69becf3 | |||
| d460d3ccdf | |||
| 0ee74943e8 | |||
| 82837e38d2 | |||
| 91df90d760 | |||
| 1ee37da720 | |||
| 6f8fc5b69d | |||
| be1918c769 | |||
| 0057b35a0a | |||
| d12974702a | |||
| 921c41689d | |||
| 6389899507 | |||
| 92ede2a2b3 | |||
| 826a63ed71 | |||
| d1576a2a72 | |||
| 95f218d704 | |||
| 315d3a0ac0 | |||
| 954818cef2 | |||
| 5a95ca9b1b | |||
| c1e6f4c26e | |||
| 7c603258fb | |||
| fcbc582686 | |||
| 9c4906b90b | |||
| d35e31e02d | |||
| 6c4ca36c09 | |||
| 70355dc505 | |||
| 20aeaf2a05 | |||
| 1f13e1d08b | |||
| eb9d35c123 | |||
| 45808cd530 | |||
| fd715a3f92 | |||
| e3b32367a7 | |||
| bf9554a746 | |||
| b9f8ca1488 | |||
| e8c4159678 | |||
| e57e71629e | |||
| 2bfd9d24a4 | |||
| b18b8fe22f | |||
| b154ff2f9d | |||
| e9b764b63c | |||
| 7216b97d92 | |||
| 0e9d93b194 | |||
| 2cbee62f0a | |||
| 379e9da5fe | |||
| ae7caa4901 | |||
| cd8b4c9dd7 | |||
| 60cd61dfd2 | |||
| 5a6637d9fc | |||
| dca7ed23f5 | |||
| 8cb5791f3b | |||
| 9b67f2c58f | |||
| 3815810d4f | |||
| 819ceba17d | |||
| 0aa0d62e03 | |||
| b3e2a35880 | |||
| 15ec20c65d | |||
| d4d6d7e12f | |||
| 2e7a4f1421 | |||
| ab8a52faf3 | |||
| bd465559fb | |||
| 13ec45a95c | |||
| 13b034875b | |||
| bb701fb088 | |||
| b367414865 | |||
| 4b00eec608 | |||
| 5e11b51152 | |||
| 9fb43b996e | |||
| bc2072e81f | |||
| f36bc3f643 | |||
| f7901ad843 | |||
| 3771030ed6 | |||
| 57197e58b5 | |||
| ac601399ac | |||
| 6a0e221153 | |||
| 6a949fc851 | |||
| f1a077dc2f | |||
| f20962b02b | |||
| 77e057f244 | |||
| 2c5114c770 | |||
| a30895a199 | |||
| 6d46d4b3da | |||
| 006efbbb77 | |||
| 52334087ad | |||
| ea8fb2c70a | |||
| 531d148718 | |||
| d83ddea323 | |||
| b5dea34090 | |||
| 7d8e3202b5 | |||
| d956e0af4b | |||
| 9ad70990b5 | |||
| 82d711f077 | |||
| 5fc28ef711 | |||
| 1f3dc682e2 | |||
| cce7768246 | |||
| 01172b0664 | |||
| 1ca11fdd34 | |||
| 52a064ae45 | |||
| 6c62ddf927 | |||
| 47e04c08d0 | |||
| d1601874b4 | |||
| 6bc19af041 | |||
| 4e72a3886f | |||
| 8e6b219eea | |||
| bd638251e4 | |||
| 0173d5e4b3 | |||
| 4f364e134b | |||
| 94d2519717 | |||
| 6a11d2e357 | |||
| 0a9e0bc9e4 | |||
| 5914e367d1 | |||
| aeaed888d4 | |||
| a0f054b005 | |||
| 4498e9d444 | |||
| 43e0763fef | |||
| a519c8a482 | |||
| 19bc595a2a | |||
| db07825c58 | |||
| b199e3bf0e | |||
| 1f9ea0891e | |||
| 0a7aa48f1e | |||
| 4b65b7da6c | |||
| 183a7dac4b | |||
| 6726f008c1 | |||
| 89cf0c140f | |||
| 504025ce80 | |||
| fee9e914f1 | |||
| 76efa71c68 | |||
| 26e61959ae | |||
| 9a8956ef9d | |||
| 10d3ffc2f6 | |||
| 090399f61d | |||
| ae7d975a92 | |||
| 55ec6bcafe | |||
| f0566d15af | |||
| a730b692bc | |||
| 826d767423 | |||
| 6d227c7fcd | |||
| 7d9d97840e | |||
| 110ded45a0 | |||
| 7872b593c7 | |||
| 90be30bddb | |||
| a298c61dab | |||
| eb416c45bd | |||
| b05b817aeb | |||
| 666602283a | |||
| ac040a4bae | |||
| b4982c8f22 | |||
| ce3ad92095 | |||
| 8e1ac8698c | |||
| b60a39c7cb | |||
| 3b3e8ba4c8 | |||
| e387f6d3be | |||
| 799d469cb6 | |||
| a54a596fa7 | |||
| 92d73d0285 | |||
| acb752e4e8 | |||
| 9e377abba6 | |||
| 04552c0923 | |||
| 2e1fb85b73 | |||
| 50db32d9b4 | |||
| 28743d953e | |||
| b109d26aa7 | |||
| 7fd21bd06d | |||
| 12e0ffb466 | |||
| 9259341df8 | |||
| 8e8aca7e7b | |||
| 7f0bcd987b | |||
| ef21de95cb | |||
| ca3246de02 | |||
| f0940b7926 | |||
| 0066e0b901 | |||
| 9771f566b0 | |||
| 38ad4c6dec | |||
| 37cf80a188 | |||
| c86ee53f66 | |||
| c2cea7e797 | |||
| a8ef6cdd4f | |||
| 53d157fee8 | |||
| c2e07b13f6 | |||
| 2e8cc48311 | |||
| f6f811eb77 | |||
| ac5528fb15 | |||
| 940d2b7862 | |||
| 835fe3dad3 | |||
| dfaecc08c5 | |||
| 87f5e9b847 | |||
| 3d3939e808 | |||
| 90822e3858 | |||
| 14eec47e9c | |||
| 15ed3fcc69 | |||
| fd8fa9f3ef | |||
| b81075f4a7 | |||
| f11a52e8e1 | |||
| 9c007483d4 | |||
| ff4e818e4c | |||
| 45a50ca0c1 | |||
| 65d9021c37 | |||
| 66481a0391 | |||
| a14a82bc9a | |||
| 756c57a16e | |||
| 8b19e34dc5 | |||
| 50083019ee | |||
| 155272e638 | |||
| 08443ceb3d | |||
| c215696f04 | |||
| 5ca42bf9b6 | |||
| 3272b9dec5 | |||
| 2ebd5da4aa | |||
| 34f024ace2 | |||
| b31f2d50f6 | |||
| da44d3b2b4 | |||
| 99ec2aca6a | |||
| 6c278604ec | |||
| 1e094a467a | |||
| 978ccfeeba | |||
| e93d66d8a1 | |||
| c29a749833 | |||
| b08d5d1261 | |||
| 9b129789e9 | |||
| a76a6d2798 | |||
| 086a760378 | |||
| f78c8d4fd8 | |||
| 7b91489997 | |||
| 36a8980c95 | |||
| 7c65640cb7 | |||
| d70e68495a | |||
| 2586202772 | |||
| b5f771368a | |||
| 0c28320ce3 | |||
| c8b4fbc36b | |||
| e9b07849fe | |||
| 409260af6f | |||
| d3d53d1a4e | |||
| e2db191f70 | |||
| d61816734d | |||
| f4dad8058f | |||
| 70bdb375c3 | |||
| e724ab0a29 | |||
| 7d0ee2ac11 | |||
| 59b7e852e2 | |||
| b2eb1a391d | |||
| 9b3aee98d3 | |||
| 0476f4144c | |||
| ed77f45fae | |||
| 0cd529e746 | |||
| 5969048318 | |||
| d1a7f8baa0 | |||
| 18dc936002 | |||
| b4b7b5d572 | |||
| 291c2e692d | |||
| 8a9a4f21b1 | |||
| cd31332b39 | |||
| cc8d2162a0 | |||
| e6313cdc67 | |||
| 5af64892e7 | |||
| a5578a7ac7 | |||
| fcdda6406e | |||
| 9bdd9f8aa6 | |||
| f3856f051b | |||
| d3a6662c60 | |||
| 5474eddf84 | |||
| b666cd47d4 | |||
| 8a986383fe | |||
| 9fa17f617e | |||
| e46e165704 | |||
| a6af423fb4 | |||
| 3397e694c0 | |||
| 77ff82505e | |||
| c3f2838270 | |||
| aed7f205b6 | |||
| 95b3587f7a | |||
| a4baeb995a | |||
| 032ab54206 | |||
| 1f9c1eb1c0 | |||
| a213e568ba | |||
| 7aeaeb4b86 | |||
| 81aef4b8fa | |||
| 31f0b6a16c | |||
| 44e3a682fd |
@@ -45,10 +45,6 @@ jobs:
|
|||||||
mkdir -p ~/.gradle
|
mkdir -p ~/.gradle
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Download android.jar
|
|
||||||
run: |
|
|
||||||
cd master
|
|
||||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
|
||||||
|
|
||||||
- name: Build Jar
|
- name: Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
|||||||
@@ -47,11 +47,6 @@ jobs:
|
|||||||
mkdir -p ~/.gradle
|
mkdir -p ~/.gradle
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Download android.jar
|
|
||||||
run: |
|
|
||||||
cd master
|
|
||||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
|
||||||
|
|
||||||
- name: Build Jar
|
- name: Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
env:
|
env:
|
||||||
@@ -64,30 +59,22 @@ jobs:
|
|||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
# - 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
|
- name: Generate Tag Name
|
||||||
id: GenTagName
|
id: GenTagName
|
||||||
run: |
|
run: |
|
||||||
cd master/server/build
|
cd master/server/build
|
||||||
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
|
genTag=$(ls *.jar | sed -e's/Tachidesk-Server-\|.jar//g')
|
||||||
echo "$genTag"
|
echo "$genTag"
|
||||||
echo "::set-output name=value::$genTag"
|
echo "::set-output name=value::$genTag"
|
||||||
|
|
||||||
- name: make windows packages
|
- name: make bundle packages
|
||||||
run: |
|
run: |
|
||||||
cd master/scripts
|
cd master/scripts
|
||||||
./windows-bundler.sh win32
|
./windows-bundler.sh win32
|
||||||
./windows-bundler.sh win64
|
./windows-bundler.sh win64
|
||||||
|
./unix-bundler.sh linux-x64
|
||||||
# - name: Mock make windows packages
|
./unix-bundler.sh macOS-x64
|
||||||
# run: |
|
./unix-bundler.sh macOS-arm64
|
||||||
# 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
|
||||||
@@ -116,11 +103,7 @@ jobs:
|
|||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
|
artifacts: "master/server/build/*.jar,master/server/build/*.zip,master/server/build/*.tar.gz"
|
||||||
owner: "Suwayomi"
|
owner: "Suwayomi"
|
||||||
repo: "Tachidesk-Server-preview"
|
repo: "Tachidesk-Server-preview"
|
||||||
tag: ${{ steps.GenTagName.outputs.value }}
|
tag: ${{ steps.GenTagName.outputs.value }}
|
||||||
|
|
||||||
- name: Run Docker build workflow
|
|
||||||
run: |
|
|
||||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "preview"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
name: Docker Build Stable
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_publish_docker_container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: run docker build and publish script
|
||||||
|
run: |
|
||||||
|
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
|
||||||
|
|
||||||
@@ -46,18 +46,6 @@ jobs:
|
|||||||
mkdir -p ~/.gradle
|
mkdir -p ~/.gradle
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Download android.jar
|
|
||||||
run: |
|
|
||||||
cd master
|
|
||||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
|
||||||
|
|
||||||
- name: Cache node_modules
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
**/webUI/node_modules
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
env:
|
env:
|
||||||
@@ -70,23 +58,21 @@ jobs:
|
|||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
- name: make windows packages
|
- name: make bundle packages
|
||||||
run: |
|
run: |
|
||||||
cd master/scripts
|
cd master/scripts
|
||||||
./windows-bundler.sh win32
|
./windows-bundler.sh win32
|
||||||
./windows-bundler.sh win64
|
./windows-bundler.sh win64
|
||||||
|
./unix-bundler.sh linux-x64
|
||||||
|
./unix-bundler.sh macOS-x64
|
||||||
|
./unix-bundler.sh macOS-arm64
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@v1
|
uses: xresloader/upload-to-github-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
file: "master/server/build/*.jar;master/server/build/*.zip"
|
file: "master/server/build/*.jar;master/server/build/*.zip;master/server/build/*.tar.gz"
|
||||||
tags: true
|
tags: true
|
||||||
draft: true
|
draft: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
- name: Run Docker build workflow
|
|
||||||
run: |
|
|
||||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
|
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -1,16 +1,21 @@
|
|||||||
# Ignore Gradle project-specific cache directory
|
# Ignore project-specific local files and dirs
|
||||||
.gradle
|
.gradle
|
||||||
.idea
|
.idea
|
||||||
gradle.properties
|
gradle.properties
|
||||||
|
|
||||||
|
# But we need these
|
||||||
|
!.idea/runConfigurations
|
||||||
|
|
||||||
# Ignore Gradle build output directory
|
# Ignore Gradle build output directory
|
||||||
build
|
build
|
||||||
|
server/out
|
||||||
|
AndroidCompat/out
|
||||||
|
|
||||||
|
# WebUI is either to be downloaded on-demand or is a dynamic build asset
|
||||||
server/src/main/resources/WebUI.zip
|
server/src/main/resources/WebUI.zip
|
||||||
server/tmp/
|
|
||||||
server/tachiserver-data/
|
|
||||||
|
|
||||||
# bundle asset downlaods
|
# bundling stage downlaoded assets
|
||||||
OpenJDK*.zip
|
scripts/OpenJDK*
|
||||||
electron-*.zip
|
scripts/zulu*
|
||||||
rcedit-*
|
scripts/electron-*
|
||||||
|
scripts/rcedit-*
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
// Config API, moved to the global build.gradle
|
|
||||||
// implementation("com.typesafe:config:1.4.0")
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
package xyz.nulldev.ts.config
|
package xyz.nulldev.ts.config
|
||||||
|
|
||||||
import net.harawata.appdirs.AppDirsFactory
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
|
|
||||||
|
const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
|
||||||
|
|
||||||
val ApplicationRootDir: String
|
val ApplicationRootDir: String
|
||||||
get(): String {
|
get(): String {
|
||||||
return System.getProperty(
|
return System.getProperty(
|
||||||
"suwayomi.tachidesk.server.rootDir",
|
"$CONFIG_PREFIX.server.rootDir",
|
||||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.kodein.di.singleton
|
|||||||
|
|
||||||
class ConfigKodeinModule {
|
class ConfigKodeinModule {
|
||||||
fun create() = DI.Module("ConfigManager") {
|
fun create() = DI.Module("ConfigManager") {
|
||||||
//Config module
|
// Config module
|
||||||
bind<ConfigManager>() with singleton { GlobalConfigManager }
|
bind<ConfigManager>() with singleton { GlobalConfigManager }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ open class ConfigManager {
|
|||||||
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||||
val config by lazy { loadConfigs() }
|
val config by lazy { loadConfigs() }
|
||||||
|
|
||||||
//Public read-only view of modules
|
// Public read-only view of modules
|
||||||
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
||||||
get() = generatedModules
|
get() = generatedModules
|
||||||
|
|
||||||
@@ -35,35 +35,35 @@ open class ConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Get a config module (Java API)
|
* Get a config module (Java API)
|
||||||
*/
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
|
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configs
|
* Load configs
|
||||||
*/
|
*/
|
||||||
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 =
|
val baseConfig =
|
||||||
ConfigFactory.parseMap(
|
ConfigFactory.parseMap(
|
||||||
mapOf(
|
mapOf(
|
||||||
"ts.server.rootDir" to ApplicationRootDir
|
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
//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(baseConfig)
|
||||||
.withFallback(userConfig)
|
.withFallback(userConfig)
|
||||||
.withFallback(compatConfig)
|
.withFallback(compatConfig)
|
||||||
.withFallback(serverConfig)
|
.withFallback(serverConfig)
|
||||||
.resolve()
|
.resolve()
|
||||||
|
|
||||||
// set log level early
|
// set log level early
|
||||||
if (debugLogsEnabled(config)) {
|
if (debugLogsEnabled(config)) {
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
package xyz.nulldev.ts.config
|
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 com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import io.github.config4k.getValue
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract config module.
|
* Abstract config module.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
abstract class ConfigModule(config: Config)
|
abstract class ConfigModule(config: Config)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract jvm-commandline-argument-overridable config module.
|
||||||
|
*/
|
||||||
|
abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String) : ConfigModule(config) {
|
||||||
|
val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
|
||||||
|
class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String) {
|
||||||
|
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
|
||||||
|
val configValue: T = config.getValue(thisRef, property)
|
||||||
|
|
||||||
|
val combined = System.getProperty(
|
||||||
|
"$CONFIG_PREFIX.$moduleName.${property.name}",
|
||||||
|
configValue.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
return when (T::class.simpleName) {
|
||||||
|
"Int" -> combined.toInt()
|
||||||
|
"Boolean" -> combined.toBoolean()
|
||||||
|
// add more types as needed
|
||||||
|
else -> combined // covers String
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ fun setLogLevel(level: Level) {
|
|||||||
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
|
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
|
||||||
}
|
}
|
||||||
|
|
||||||
fun debugLogsEnabled(config: Config)
|
fun debugLogsEnabled(config: Config) =
|
||||||
= System.getProperty("suwayomi.tachidesk.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ package xyz.nulldev.ts.config.util
|
|||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
|
||||||
operator fun Config.get(key: String) = getString(key)
|
operator fun Config.get(key: String) = getString(key)
|
||||||
?: throw IllegalStateException("Could not find value for config entry: $key!")
|
?: throw IllegalStateException("Could not find value for config entry: $key!")
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
xyz.nulldev.ts.api.v2.java.impl.ServerAPIImpl
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"label": "Sync",
|
|
||||||
"icon": "import_export",
|
|
||||||
"type": "nested",
|
|
||||||
"prefs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Server",
|
|
||||||
"icon": "dns",
|
|
||||||
"type": "nested",
|
|
||||||
"prefs": [
|
|
||||||
{
|
|
||||||
"label": "Password authentication",
|
|
||||||
"type": "text-password",
|
|
||||||
"default": "",
|
|
||||||
"key": "pref_ts_server_password",
|
|
||||||
"hint": "Enter a password"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,77 +1,28 @@
|
|||||||
|
|
||||||
plugins {
|
|
||||||
application
|
|
||||||
kotlin("plugin.serialization")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
|
||||||
url = uri("https://jitpack.io")
|
|
||||||
}
|
|
||||||
|
|
||||||
maven {
|
|
||||||
url = uri("https://maven.google.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Android stub library
|
// Android stub library
|
||||||
implementation(fileTree("lib/"))
|
implementation("com.github.Suwayomi:android-jar:1.0.0")
|
||||||
|
|
||||||
|
|
||||||
// Android JAR libs
|
|
||||||
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
|
|
||||||
|
|
||||||
// JSON
|
|
||||||
compileOnly("com.google.code.gson:gson:2.8.6")
|
|
||||||
|
|
||||||
// Javassist
|
|
||||||
compileOnly("org.javassist:javassist:3.27.0-GA")
|
|
||||||
|
|
||||||
// XML
|
// XML
|
||||||
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
compileOnly("xmlpull:xmlpull:1.1.3.4a")
|
||||||
|
|
||||||
// Config API
|
// Config API
|
||||||
implementation(project(":AndroidCompat:Config"))
|
implementation(project(":AndroidCompat:Config"))
|
||||||
|
|
||||||
// APK sig verifier
|
// APK sig verifier
|
||||||
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
compileOnly("com.android.tools.build:apksig:7.1.0-alpha12")
|
||||||
|
|
||||||
// AndroidX annotations
|
// AndroidX annotations
|
||||||
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
|
compileOnly("androidx.annotation:annotation:1.2.0")
|
||||||
|
|
||||||
// substitute for duktape-android
|
// substitute for duktape-android
|
||||||
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
|
implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino'
|
||||||
implementation("org.mozilla:rhino-runtime:1.7.13")
|
implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
|
||||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
|
||||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
|
||||||
|
|
||||||
// Kotlin wrapper around Java Preferences, makes certain things easier
|
// Kotlin wrapper around Java Preferences, makes certain things easier
|
||||||
val multiplatformSettingsVersion = "0.7.7"
|
val multiplatformSettingsVersion = "0.8"
|
||||||
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
|
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
|
||||||
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
|
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
|
||||||
|
|
||||||
// Android version of SimpleDateFormat
|
// Android version of SimpleDateFormat
|
||||||
implementation("com.ibm.icu:icu4j:69.1")
|
implementation("com.ibm.icu:icu4j:69.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
|
||||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
|
||||||
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
|
||||||
//
|
|
||||||
//// Copy JVM core patches
|
|
||||||
//task copyJVMPatches(type: Copy) {
|
|
||||||
// from fatJarTask.outputs.files
|
|
||||||
// into 'src/main/resources/patches'
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//compileOnly(Java.dependsOn gradle.includedBuild('dex2jar').task(':dex-translator:assemble')
|
|
||||||
//compileOnly(Java.dependsOn copyJVMPatches
|
|
||||||
//copyJVMPatches.dependsOn fatJarTask
|
|
||||||
//
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
android.jar
|
|
||||||
@@ -9,8 +9,10 @@ import android.content.Context
|
|||||||
class PreferenceManager {
|
class PreferenceManager {
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getDefaultSharedPreferences(context: Context)
|
fun getDefaultSharedPreferences(context: Context) =
|
||||||
= context.getSharedPreferences(context.applicationInfo.packageName,
|
context.getSharedPreferences(
|
||||||
Context.MODE_PRIVATE)!!
|
context.applicationInfo.packageName,
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,33 +13,49 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class MultiSelectListPreference extends DialogPreference {
|
public class MultiSelectListPreference extends DialogPreference {
|
||||||
|
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/MultiSelectListPreference.java
|
||||||
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
||||||
|
|
||||||
public MultiSelectListPreference(Context context) { super(context); }
|
private CharSequence[] entries;
|
||||||
|
private CharSequence[] entryValues;
|
||||||
|
|
||||||
|
public MultiSelectListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEntries(CharSequence[] entries) {
|
||||||
|
this.entries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence[] getEntries() {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEntryValues(CharSequence[] entryValues) {
|
||||||
|
this.entryValues = entryValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence[] getEntryValues() {
|
||||||
|
return entryValues;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public void setEntries(CharSequence[] entries) { throw new RuntimeException("Stub!"); }
|
public void setValues(Set<String> values) {
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public CharSequence[] getEntries() { throw new RuntimeException("Stub!"); }
|
public Set<String> getValues() {
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
public int findIndexOfValue(String value) {
|
||||||
public void setEntryValues(CharSequence[] entryValues) { throw new RuntimeException("Stub!"); }
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
@JsonIgnore
|
|
||||||
public CharSequence[] getEntryValues() { throw new RuntimeException("Stub!"); }
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public void setValues(Set<String> values) { throw new RuntimeException("Stub!"); }
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public Set<String> getValues() { throw new RuntimeException("Stub!"); }
|
|
||||||
|
|
||||||
public int findIndexOfValue(String value) { throw new RuntimeException("Stub!"); }
|
|
||||||
|
|
||||||
/** Tachidesk specific API */
|
/** Tachidesk specific API */
|
||||||
@Override
|
@Override
|
||||||
public String getDefaultValueType() {
|
public String getDefaultValueType() {
|
||||||
return "Set";
|
return "Set<String>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ package androidx.preference;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A minimal implementation of androidx.preference.Preference
|
* A minimal implementation of androidx.preference.Preference
|
||||||
@@ -113,18 +114,22 @@ public class Preference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Tachidesk specific API */
|
/** Tachidesk specific API */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public Object getCurrentValue() {
|
public Object getCurrentValue() {
|
||||||
switch (getDefaultValueType()) {
|
switch (getDefaultValueType()) {
|
||||||
case "String":
|
case "String":
|
||||||
return sharedPreferences.getString(key, (String)defaultValue);
|
return sharedPreferences.getString(key, (String)defaultValue);
|
||||||
case "Boolean":
|
case "Boolean":
|
||||||
return sharedPreferences.getBoolean(key, (Boolean)defaultValue);
|
return sharedPreferences.getBoolean(key, (Boolean)defaultValue);
|
||||||
|
case "Set<String>":
|
||||||
|
return sharedPreferences.getStringSet(key, (Set<String>)defaultValue);
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Unsupported type");
|
throw new RuntimeException("Unsupported type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tachidesk specific API */
|
/** Tachidesk specific API */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public void saveNewValue(Object value) {
|
public void saveNewValue(Object value) {
|
||||||
switch (getDefaultValueType()) {
|
switch (getDefaultValueType()) {
|
||||||
case "String":
|
case "String":
|
||||||
@@ -133,6 +138,9 @@ public class Preference {
|
|||||||
case "Boolean":
|
case "Boolean":
|
||||||
sharedPreferences.edit().putBoolean(key, (Boolean)value).apply();
|
sharedPreferences.edit().putBoolean(key, (Boolean)value).apply();
|
||||||
break;
|
break;
|
||||||
|
case "Set<String>":
|
||||||
|
sharedPreferences.edit().putStringSet(key, (Set<String>)value).apply();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Unsupported type");
|
throw new RuntimeException("Unsupported type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||||||
public class TwoStatePreference extends Preference {
|
public class TwoStatePreference extends Preference {
|
||||||
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
||||||
|
|
||||||
public TwoStatePreference(Context context) { super(context); }
|
public TwoStatePreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
setDefaultValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public boolean isChecked() { throw new RuntimeException("Stub!"); }
|
public boolean isChecked() { throw new RuntimeException("Stub!"); }
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
package com.f2prateek;
|
|
||||||
//TODO Consider if we can change this package into an Android dependency
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file may have been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
final class BooleanAdapter implements Preference.Adapter<Boolean> {
|
|
||||||
static final BooleanAdapter INSTANCE = new BooleanAdapter();
|
|
||||||
|
|
||||||
@Override public Boolean get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
return preferences.getBoolean(key, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void set(@NonNull String key, @NonNull Boolean value,
|
|
||||||
@NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putBoolean(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file may have been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
final class EnumAdapter<T extends Enum<T>> implements Preference.Adapter<T> {
|
|
||||||
private final Class<T> enumClass;
|
|
||||||
|
|
||||||
EnumAdapter(Class<T> enumClass) {
|
|
||||||
this.enumClass = enumClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public T get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
String value = preferences.getString(key, null);
|
|
||||||
assert value != null; // Not called unless key is present.
|
|
||||||
return Enum.valueOf(enumClass, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putString(key, value.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file may have been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
final class FloatAdapter implements Preference.Adapter<Float> {
|
|
||||||
static final FloatAdapter INSTANCE = new FloatAdapter();
|
|
||||||
|
|
||||||
@Override public Float get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
return preferences.getFloat(key, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void set(@NonNull String key, @NonNull Float value,
|
|
||||||
@NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putFloat(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file may have been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
final class IntegerAdapter implements Preference.Adapter<Integer> {
|
|
||||||
static final IntegerAdapter INSTANCE = new IntegerAdapter();
|
|
||||||
|
|
||||||
@Override public Integer get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
return preferences.getInt(key, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void set(@NonNull String key, @NonNull Integer value,
|
|
||||||
@NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putInt(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file may have been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
final class LongAdapter implements Preference.Adapter<Long> {
|
|
||||||
static final LongAdapter INSTANCE = new LongAdapter();
|
|
||||||
|
|
||||||
@Override public Long get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
return preferences.getLong(key, 0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void set(@NonNull String key, @NonNull Long value,
|
|
||||||
@NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putLong(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file has been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.CheckResult;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import rx.Observable;
|
|
||||||
import rx.functions.Action1;
|
|
||||||
|
|
||||||
/** A preference of type {@link T}. Instances can be created from {@link RxSharedPreferences}. */
|
|
||||||
public final class Preference<T> {
|
|
||||||
/** Stores and retrieves instances of {@code T} in {@link SharedPreferences}. */
|
|
||||||
public interface Adapter<T> {
|
|
||||||
/** Retrieve the value for {@code key} from {@code preferences}. */
|
|
||||||
T get(@NonNull String key, @NonNull SharedPreferences preferences);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store non-null {@code value} for {@code key} in {@code editor}.
|
|
||||||
* <p>
|
|
||||||
* Note: Implementations <b>must not</b> call {@code commit()} or {@code apply()} on
|
|
||||||
* {@code editor}.
|
|
||||||
*/
|
|
||||||
void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final SharedPreferences preferences;
|
|
||||||
private final String key;
|
|
||||||
private final T defaultValue;
|
|
||||||
private final Adapter<T> adapter;
|
|
||||||
private final Observable<T> values;
|
|
||||||
|
|
||||||
Preference(SharedPreferences preferences, final String key, T defaultValue, Adapter<T> adapter,
|
|
||||||
Observable<String> keyChanges) {
|
|
||||||
this.preferences = preferences;
|
|
||||||
this.key = key;
|
|
||||||
this.defaultValue = defaultValue;
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.values = keyChanges
|
|
||||||
.filter(key::equals)
|
|
||||||
.startWith("<init>") // Dummy value to trigger initial load.
|
|
||||||
.onBackpressureLatest()
|
|
||||||
.map(ignored -> get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The key for which this preference will store and retrieve values. */
|
|
||||||
@NonNull
|
|
||||||
public String key() {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The value used if none is stored. May be {@code null}. */
|
|
||||||
@Nullable
|
|
||||||
public T defaultValue() {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the current value for this preference. Returns {@link #defaultValue()} if no value is
|
|
||||||
* set.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public T get() {
|
|
||||||
if (!preferences.contains(key)) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
return adapter.get(key, preferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change this preference's stored value to {@code value}. A value of {@code null} will delete the
|
|
||||||
* preference.
|
|
||||||
*/
|
|
||||||
public void set(@Nullable T value) {
|
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
|
||||||
if (value == null) {
|
|
||||||
editor.remove(key);
|
|
||||||
} else {
|
|
||||||
adapter.set(key, value, editor);
|
|
||||||
}
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns true if this preference has a stored value. */
|
|
||||||
public boolean isSet() {
|
|
||||||
return preferences.contains(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Delete the stored value for this preference, if any. */
|
|
||||||
public void delete() {
|
|
||||||
set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observe changes to this preference. The current value or {@link #defaultValue()} will be
|
|
||||||
* emitted on first subscribe.
|
|
||||||
*/
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Observable<T> asObservable() {
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An action which stores a new value for this preference. Passing {@code null} will delete the
|
|
||||||
* preference.
|
|
||||||
*/
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Action1<? super T> asAction() {
|
|
||||||
return (Action1<T>) this::set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
This file has been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
|
||||||
import android.support.annotation.CheckResult;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
import rx.Observable;
|
|
||||||
import rx.subscriptions.Subscriptions;
|
|
||||||
|
|
||||||
import static android.os.Build.VERSION_CODES.HONEYCOMB;
|
|
||||||
import static com.f2prateek.rx.preferences.Preconditions.checkNotNull;
|
|
||||||
|
|
||||||
/** A factory for reactive {@link Preference} objects. */
|
|
||||||
public final class RxSharedPreferences {
|
|
||||||
private static final Float DEFAULT_FLOAT = 0f;
|
|
||||||
private static final Integer DEFAULT_INTEGER = 0;
|
|
||||||
private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE;
|
|
||||||
private static final Long DEFAULT_LONG = 0L;
|
|
||||||
|
|
||||||
/** Create an instance of {@link RxSharedPreferences} for {@code preferences}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public static RxSharedPreferences create(@NonNull SharedPreferences preferences) {
|
|
||||||
checkNotNull(preferences, "preferences == null");
|
|
||||||
return new RxSharedPreferences(preferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final SharedPreferences preferences;
|
|
||||||
private final Observable<String> keyChanges;
|
|
||||||
|
|
||||||
private RxSharedPreferences(final SharedPreferences preferences) {
|
|
||||||
this.preferences = preferences;
|
|
||||||
this.keyChanges = Observable.create((Observable.OnSubscribe<String>) subscriber -> {
|
|
||||||
final OnSharedPreferenceChangeListener listener = (preferences1, key) -> subscriber.onNext(key);
|
|
||||||
|
|
||||||
preferences.registerOnSharedPreferenceChangeListener(listener);
|
|
||||||
|
|
||||||
subscriber.add(Subscriptions.create(() -> preferences.unregisterOnSharedPreferenceChangeListener(listener)));
|
|
||||||
}).share();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a boolean preference for {@code key}. Default is {@code false}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Boolean> getBoolean(@NonNull String key) {
|
|
||||||
return getBoolean(key, DEFAULT_BOOLEAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a boolean preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Boolean> getBoolean(@NonNull String key, @Nullable Boolean defaultValue) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, BooleanAdapter.INSTANCE, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create an enum preference for {@code key}. Default is {@code null}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key,
|
|
||||||
@NonNull Class<T> enumClass) {
|
|
||||||
return getEnum(key, null, enumClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create an enum preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key, @Nullable T defaultValue,
|
|
||||||
@NonNull Class<T> enumClass) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
checkNotNull(enumClass, "enumClass == null");
|
|
||||||
Preference.Adapter<T> adapter = new EnumAdapter<>(enumClass);
|
|
||||||
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a float preference for {@code key}. Default is {@code 0}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Float> getFloat(@NonNull String key) {
|
|
||||||
return getFloat(key, DEFAULT_FLOAT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a float preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Float> getFloat(@NonNull String key, @Nullable Float defaultValue) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, FloatAdapter.INSTANCE, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create an integer preference for {@code key}. Default is {@code 0}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Integer> getInteger(@NonNull String key) {
|
|
||||||
//noinspection UnnecessaryBoxing
|
|
||||||
return getInteger(key, DEFAULT_INTEGER);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create an integer preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Integer> getInteger(@NonNull String key, @Nullable Integer defaultValue) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, IntegerAdapter.INSTANCE, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a long preference for {@code key}. Default is {@code 0}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Long> getLong(@NonNull String key) {
|
|
||||||
//noinspection UnnecessaryBoxing
|
|
||||||
return getLong(key, DEFAULT_LONG);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a long preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Long> getLong(@NonNull String key, @Nullable Long defaultValue) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, LongAdapter.INSTANCE, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a preference of type {@code T} for {@code key}. Default is {@code null}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public <T> Preference<T> getObject(@NonNull String key, @NonNull Preference.Adapter<T> adapter) {
|
|
||||||
return getObject(key, null, adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a preference for type {@code T} for {@code key} with a default of {@code defaultValue}.
|
|
||||||
*/
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public <T> Preference<T> getObject(@NonNull String key, @Nullable T defaultValue,
|
|
||||||
@NonNull Preference.Adapter<T> adapter) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
checkNotNull(adapter, "adapter == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a string preference for {@code key}. Default is {@code null}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<String> getString(@NonNull String key) {
|
|
||||||
return getString(key, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a string preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<String> getString(@NonNull String key, @Nullable String defaultValue) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, StringAdapter.INSTANCE, keyChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a string set preference for {@code key}. Default is an empty set. */
|
|
||||||
@TargetApi(HONEYCOMB)
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Set<String>> getStringSet(@NonNull String key) {
|
|
||||||
return getStringSet(key, Collections.emptySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a string set preference for {@code key} with a default of {@code defaultValue}. */
|
|
||||||
@TargetApi(HONEYCOMB)
|
|
||||||
@CheckResult @NonNull
|
|
||||||
public Preference<Set<String>> getStringSet(@NonNull String key,
|
|
||||||
@NonNull Set<String> defaultValue) {
|
|
||||||
checkNotNull(key, "key == null");
|
|
||||||
return new Preference<>(preferences, key, defaultValue, StringSetAdapter.INSTANCE, keyChanges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
final class StringAdapter implements Preference.Adapter<String> {
|
|
||||||
static final StringAdapter INSTANCE = new StringAdapter();
|
|
||||||
|
|
||||||
@Override public String get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
return preferences.getString(key, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void set(@NonNull String key, @NonNull String value,
|
|
||||||
@NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putString(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static android.os.Build.VERSION_CODES.HONEYCOMB;
|
|
||||||
|
|
||||||
@TargetApi(HONEYCOMB)
|
|
||||||
final class StringSetAdapter implements Preference.Adapter<Set<String>> {
|
|
||||||
static final StringSetAdapter INSTANCE = new StringSetAdapter();
|
|
||||||
|
|
||||||
@Override public Set<String> get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
|
||||||
return preferences.getStringSet(key, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void set(@NonNull String key, @NonNull Set<String> value,
|
|
||||||
@NonNull SharedPreferences.Editor editor) {
|
|
||||||
editor.putStringSet(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package com.github.pwittchen.reactivenetwork.library
|
|
||||||
|
|
||||||
import android.net.NetworkInfo
|
|
||||||
|
|
||||||
class Connectivity {
|
|
||||||
val state = NetworkInfo.State.CONNECTED
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.github.pwittchen.reactivenetwork.library
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by nulldev on 12/29/16.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ReactiveNetwork {
|
|
||||||
companion object {
|
|
||||||
fun observeNetworkConnectivity(context: Context) = Observable.just(Connectivity())!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -5,4 +5,4 @@ import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory
|
|||||||
|
|
||||||
class RequerySQLiteOpenHelperFactory {
|
class RequerySQLiteOpenHelperFactory {
|
||||||
fun create(configuration: SupportSQLiteOpenHelper.Configuration) = FrameworkSQLiteOpenHelperFactory().create(configuration)
|
fun create(configuration: SupportSQLiteOpenHelper.Configuration) = FrameworkSQLiteOpenHelperFactory().create(configuration)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package kotlinx.coroutines.experimental.android
|
|
||||||
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
|
|
||||||
val UI = GlobalScope.coroutineContext
|
|
||||||
@@ -14,4 +14,4 @@ class AndroidCompat {
|
|||||||
application.attach(context)
|
application.attach(context)
|
||||||
application.onCreate()
|
application.onCreate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package xyz.nulldev.androidcompat
|
|||||||
|
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import xyz.nulldev.androidcompat.bytecode.ModApplier
|
|
||||||
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
||||||
import xyz.nulldev.androidcompat.config.FilesConfigModule
|
import xyz.nulldev.androidcompat.config.FilesConfigModule
|
||||||
import xyz.nulldev.androidcompat.config.SystemConfigModule
|
import xyz.nulldev.androidcompat.config.SystemConfigModule
|
||||||
@@ -12,15 +11,10 @@ import xyz.nulldev.ts.config.GlobalConfigManager
|
|||||||
* Initializes the Android compatibility module
|
* Initializes the Android compatibility module
|
||||||
*/
|
*/
|
||||||
class AndroidCompatInitializer {
|
class AndroidCompatInitializer {
|
||||||
|
|
||||||
val modApplier by lazy { ModApplier() }
|
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
modApplier.apply()
|
|
||||||
|
|
||||||
DI.global.addImport(AndroidCompatModule().create())
|
DI.global.addImport(AndroidCompatModule().create())
|
||||||
|
|
||||||
//Register config modules
|
// Register config modules
|
||||||
GlobalConfigManager.registerModules(
|
GlobalConfigManager.registerModules(
|
||||||
FilesConfigModule.register(GlobalConfigManager.config),
|
FilesConfigModule.register(GlobalConfigManager.config),
|
||||||
ApplicationInfoConfigModule.register(GlobalConfigManager.config),
|
ApplicationInfoConfigModule.register(GlobalConfigManager.config),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class AndroidCompatModule {
|
|||||||
|
|
||||||
bind<PackageController>() with singleton { PackageController() }
|
bind<PackageController>() with singleton { PackageController() }
|
||||||
|
|
||||||
//Context
|
// Context
|
||||||
bind<CustomContext>() with singleton { CustomContext() }
|
bind<CustomContext>() with singleton { CustomContext() }
|
||||||
bind<Context>() with singleton {
|
bind<Context>() with singleton {
|
||||||
val context: Context by DI.global.instance<CustomContext>()
|
val context: Context by DI.global.instance<CustomContext>()
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package xyz.nulldev.androidcompat.bytecode
|
|
||||||
|
|
||||||
import javassist.CtClass
|
|
||||||
import mu.KotlinLogging
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies Javassist modifications
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ModApplier {
|
|
||||||
|
|
||||||
val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
fun apply() {
|
|
||||||
logger.info { "Applying Javassist mods..." }
|
|
||||||
val modifiedClasses = mutableListOf<CtClass>()
|
|
||||||
|
|
||||||
modifiedClasses.forEach {
|
|
||||||
it.toClass()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-4
@@ -1,6 +1,7 @@
|
|||||||
package xyz.nulldev.androidcompat.config
|
package xyz.nulldev.androidcompat.config
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import io.github.config4k.getValue
|
||||||
import xyz.nulldev.ts.config.ConfigModule
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,11 +9,11 @@ import xyz.nulldev.ts.config.ConfigModule
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
|
class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
|
||||||
val packageName = config.getString("packageName")!!
|
val packageName: String by config
|
||||||
val debug = config.getBoolean("debug")
|
val debug: Boolean by config
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun register(config: Config)
|
fun register(config: Config) =
|
||||||
= ApplicationInfoConfigModule(config.getConfig("android.app"))
|
ApplicationInfoConfigModule(config.getConfig("android.app"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package xyz.nulldev.androidcompat.config
|
package xyz.nulldev.androidcompat.config
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import io.github.config4k.getValue
|
||||||
import xyz.nulldev.ts.config.ConfigModule
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,26 +9,26 @@ import xyz.nulldev.ts.config.ConfigModule
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class FilesConfigModule(config: Config) : ConfigModule(config) {
|
class FilesConfigModule(config: Config) : ConfigModule(config) {
|
||||||
val dataDir = config.getString("dataDir")!!
|
val dataDir: String by config
|
||||||
val filesDir = config.getString("filesDir")!!
|
val filesDir: String by config
|
||||||
val noBackupFilesDir = config.getString("noBackupFilesDir")!!
|
val noBackupFilesDir: String by config
|
||||||
val externalFilesDirs: MutableList<String> = config.getStringList("externalFilesDirs")!!
|
val externalFilesDirs: MutableList<String> by config
|
||||||
val obbDirs: MutableList<String> = config.getStringList("obbDirs")!!
|
val obbDirs: MutableList<String> by config
|
||||||
val cacheDir = config.getString("cacheDir")!!
|
val cacheDir: String by config
|
||||||
val codeCacheDir = config.getString("codeCacheDir")!!
|
val codeCacheDir: String by config
|
||||||
val externalCacheDirs: MutableList<String> = config.getStringList("externalCacheDirs")!!
|
val externalCacheDirs: MutableList<String> by config
|
||||||
val externalMediaDirs: MutableList<String> = config.getStringList("externalMediaDirs")!!
|
val externalMediaDirs: MutableList<String> by config
|
||||||
val rootDir = config.getString("rootDir")!!
|
val rootDir: String by config
|
||||||
val externalStorageDir = config.getString("externalStorageDir")!!
|
val externalStorageDir: String by config
|
||||||
val downloadCacheDir = config.getString("downloadCacheDir")!!
|
val downloadCacheDir: String by config
|
||||||
val databasesDir = config.getString("databasesDir")!!
|
val databasesDir: String by config
|
||||||
|
|
||||||
val prefsDir = config.getString("prefsDir")!!
|
val prefsDir: String by config
|
||||||
|
|
||||||
val packageDir = config.getString("packageDir")!!
|
val packageDir: String by config
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun register(config: Config)
|
fun register(config: Config) =
|
||||||
= FilesConfigModule(config.getConfig("android.files"))
|
FilesConfigModule(config.getConfig("android.files"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package xyz.nulldev.androidcompat.config
|
package xyz.nulldev.androidcompat.config
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import io.github.config4k.getValue
|
||||||
import xyz.nulldev.ts.config.ConfigModule
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
|
||||||
class SystemConfigModule(val config: Config) : ConfigModule(config) {
|
class SystemConfigModule(val config: Config) : ConfigModule(config) {
|
||||||
val isDebuggable = config.getBoolean("isDebuggable")
|
val isDebuggable: Boolean by config
|
||||||
|
|
||||||
val propertyPrefix = "properties."
|
val propertyPrefix = "properties."
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ class SystemConfigModule(val config: Config) : ConfigModule(config) {
|
|||||||
fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property")
|
fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun register(config: Config)
|
fun register(config: Config) =
|
||||||
= SystemConfigModule(config.getConfig("android.system"))
|
SystemConfigModule(config.getConfig("android.system"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ import java.io.InputStream
|
|||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.sql.*
|
|
||||||
import java.sql.Array
|
import java.sql.Array
|
||||||
|
import java.sql.Blob
|
||||||
|
import java.sql.Clob
|
||||||
import java.sql.Date
|
import java.sql.Date
|
||||||
import java.util.*
|
import java.sql.NClob
|
||||||
|
import java.sql.Ref
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.ResultSetMetaData
|
||||||
|
import java.sql.RowId
|
||||||
|
import java.sql.SQLXML
|
||||||
|
import java.sql.Time
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
||||||
|
|
||||||
private val cachedContent = mutableListOf<ResultSetEntry>()
|
private val cachedContent = mutableListOf<ResultSetEntry>()
|
||||||
@@ -19,7 +29,7 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
|
|
||||||
val parentMetadata = parent.metaData
|
val parentMetadata = parent.metaData
|
||||||
val columnCount = parentMetadata.columnCount
|
val columnCount = parentMetadata.columnCount
|
||||||
val columnLabels = (1 .. columnCount).map {
|
val columnLabels = (1..columnCount).map {
|
||||||
parentMetadata.getColumnLabel(it)
|
parentMetadata.getColumnLabel(it)
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
@@ -31,10 +41,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
// How can we optimize this?
|
// How can we optimize this?
|
||||||
// We need to fill the cache as the set is loaded
|
// We need to fill the cache as the set is loaded
|
||||||
|
|
||||||
//Fill cache
|
// Fill cache
|
||||||
while(parent.next()) {
|
while (parent.next()) {
|
||||||
cachedContent += ResultSetEntry().apply {
|
cachedContent += ResultSetEntry().apply {
|
||||||
for(i in 1 .. columnCount)
|
for (i in 1..columnCount)
|
||||||
data += parent.getObject(i)
|
data += parent.getObject(i)
|
||||||
}
|
}
|
||||||
resultSetLength++
|
resultSetLength++
|
||||||
@@ -50,8 +60,8 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun internalMove(row: Int) {
|
private fun internalMove(row: Int) {
|
||||||
if(cursor < 0) cursor = 0
|
if (cursor < 0) cursor = 0
|
||||||
else if(cursor > resultSetLength + 1) cursor = resultSetLength + 1
|
else if (cursor > resultSetLength + 1) cursor = resultSetLength + 1
|
||||||
else cursor = row
|
else cursor = row
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +75,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
return obj(cachedFindColumn(column))
|
return obj(cachedFindColumn(column))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cachedFindColumn(column: String?)
|
private fun cachedFindColumn(column: String?) =
|
||||||
= columnCache.getOrPut(column!!, {
|
columnCache.getOrPut(column!!, {
|
||||||
findColumn(column)
|
findColumn(column)
|
||||||
})
|
})
|
||||||
|
|
||||||
override fun getNClob(columnIndex: Int): NClob {
|
override fun getNClob(columnIndex: Int): NClob {
|
||||||
return obj(columnIndex) as NClob
|
return obj(columnIndex) as NClob
|
||||||
@@ -147,27 +157,27 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getDate(columnIndex: Int): Date {
|
override fun getDate(columnIndex: Int): Date {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDate(columnLabel: String?): Date {
|
override fun getDate(columnLabel: String?): Date {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDate(columnIndex: Int, cal: Calendar?): Date {
|
override fun getDate(columnIndex: Int, cal: Calendar?): Date {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDate(columnLabel: String?, cal: Calendar?): Date {
|
override fun getDate(columnLabel: String?, cal: Calendar?): Date {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeFirst() {
|
override fun beforeFirst() {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +202,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal {
|
override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal {
|
override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,22 +236,22 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTime(columnIndex: Int): Time {
|
override fun getTime(columnIndex: Int): Time {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTime(columnLabel: String?): Time {
|
override fun getTime(columnLabel: String?): Time {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTime(columnIndex: Int, cal: Calendar?): Time {
|
override fun getTime(columnIndex: Int, cal: Calendar?): Time {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTime(columnLabel: String?, cal: Calendar?): Time {
|
override fun getTime(columnLabel: String?, cal: Calendar?): Time {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,28 +272,28 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun absolute(row: Int): Boolean {
|
override fun absolute(row: Int): Boolean {
|
||||||
if(row > 0) {
|
if (row > 0) {
|
||||||
internalMove(row)
|
internalMove(row)
|
||||||
} else {
|
} else {
|
||||||
last()
|
last()
|
||||||
for(i in 1 .. row)
|
for (i in 1..row)
|
||||||
previous()
|
previous()
|
||||||
}
|
}
|
||||||
return cursorValid()
|
return cursorValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSQLXML(columnIndex: Int): SQLXML? {
|
override fun getSQLXML(columnIndex: Int): SQLXML? {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSQLXML(columnLabel: String?): SQLXML? {
|
override fun getSQLXML(columnLabel: String?): SQLXML? {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T : Any?> unwrap(iface: Class<T>?): T {
|
override fun <T : Any?> unwrap(iface: Class<T>?): T {
|
||||||
if(thisIsWrapperFor(iface))
|
if (thisIsWrapperFor(iface))
|
||||||
return this as T
|
return this as T
|
||||||
else
|
else
|
||||||
return parent.unwrap(iface)
|
return parent.unwrap(iface)
|
||||||
@@ -416,12 +426,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getBlob(columnIndex: Int): Blob {
|
override fun getBlob(columnIndex: Int): Blob {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBlob(columnLabel: String?): Blob {
|
override fun getBlob(columnLabel: String?): Blob {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,12 +500,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any {
|
override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any {
|
override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,9 +531,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun castToLong(obj: Any?): Long {
|
private fun castToLong(obj: Any?): Long {
|
||||||
if(obj == null) return 0
|
if (obj == null) return 0
|
||||||
else if(obj is Long) return obj
|
else if (obj is Long) return obj
|
||||||
else if(obj is Number) return obj.toLong()
|
else if (obj is Number) return obj.toLong()
|
||||||
else throw IllegalStateException("Object is not a long!")
|
else throw IllegalStateException("Object is not a long!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,12 +546,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getClob(columnIndex: Int): Clob {
|
override fun getClob(columnIndex: Int): Clob {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getClob(columnLabel: String?): Clob {
|
override fun getClob(columnLabel: String?): Clob {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,12 +604,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getArray(columnIndex: Int): Array {
|
override fun getArray(columnIndex: Int): Array {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getArray(columnLabel: String?): Array {
|
override fun getArray(columnLabel: String?): Array {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,32 +688,32 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimestamp(columnIndex: Int): Timestamp {
|
override fun getTimestamp(columnIndex: Int): Timestamp {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimestamp(columnLabel: String?): Timestamp {
|
override fun getTimestamp(columnLabel: String?): Timestamp {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp {
|
override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp {
|
override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRef(columnIndex: Int): Ref {
|
override fun getRef(columnIndex: Int): Ref {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRef(columnLabel: String?): Ref {
|
override fun getRef(columnLabel: String?): Ref {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,12 +792,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getRowId(columnIndex: Int): RowId {
|
override fun getRowId(columnIndex: Int): RowId {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRowId(columnLabel: String?): RowId {
|
override fun getRowId(columnLabel: String?): RowId {
|
||||||
//TODO Maybe?
|
// TODO Maybe?
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,4 +848,4 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
|||||||
class ResultSetEntry {
|
class ResultSetEntry {
|
||||||
val data = mutableListOf<Any?>()
|
val data = mutableListOf<Any?>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -12,11 +12,11 @@ import com.russhwolf.settings.ExperimentalSettingsApi
|
|||||||
import com.russhwolf.settings.ExperimentalSettingsImplementation
|
import com.russhwolf.settings.ExperimentalSettingsImplementation
|
||||||
import com.russhwolf.settings.JvmPreferencesSettings
|
import com.russhwolf.settings.JvmPreferencesSettings
|
||||||
import com.russhwolf.settings.serialization.decodeValue
|
import com.russhwolf.settings.serialization.decodeValue
|
||||||
|
import com.russhwolf.settings.serialization.decodeValueOrNull
|
||||||
import com.russhwolf.settings.serialization.encodeValue
|
import com.russhwolf.settings.serialization.encodeValue
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.builtins.SetSerializer
|
import kotlinx.serialization.builtins.SetSerializer
|
||||||
import kotlinx.serialization.builtins.nullable
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
import java.util.prefs.PreferenceChangeListener
|
import java.util.prefs.PreferenceChangeListener
|
||||||
import java.util.prefs.Preferences
|
import java.util.prefs.Preferences
|
||||||
@@ -40,13 +40,13 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
|
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? {
|
||||||
try {
|
try {
|
||||||
return if (defValues != null) {
|
return if (defValues != null) {
|
||||||
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, defValues)
|
preferences.decodeValue(SetSerializer(String.serializer()), key, defValues)
|
||||||
} else {
|
} else {
|
||||||
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, null)
|
preferences.decodeValueOrNull(SetSerializer(String.serializer()), key)
|
||||||
}?.toMutableSet()
|
}
|
||||||
} catch (e: SerializationException) {
|
} catch (e: SerializationException) {
|
||||||
throw ClassCastException("$key was not a StringSet")
|
throw ClassCastException("$key was not a StringSet")
|
||||||
}
|
}
|
||||||
@@ -174,4 +174,4 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
|
|||||||
javaPreferences.removeNode()
|
javaPreferences.removeNode()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import java.io.File
|
|||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data class InstalledPackage(val root: File) {
|
data class InstalledPackage(val root: File) {
|
||||||
val apk = File(root, "package.apk")
|
val apk = File(root, "package.apk")
|
||||||
val jar = File(root, "translated.jar")
|
val jar = File(root, "translated.jar")
|
||||||
@@ -40,20 +38,24 @@ data class InstalledPackage(val root: File) {
|
|||||||
}?.filter {
|
}?.filter {
|
||||||
it.tagName == "meta-data"
|
it.tagName == "meta-data"
|
||||||
}?.map {
|
}?.map {
|
||||||
putString(it.attributes.getNamedItem("android:name").nodeValue,
|
putString(
|
||||||
it.attributes.getNamedItem("android:value").nodeValue)
|
it.attributes.getNamedItem("android:name").nodeValue,
|
||||||
|
it.attributes.getNamedItem("android:value").nodeValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
it.signatures = (parsed.apkSingers.flatMap { it.certificateMetas }
|
it.signatures = (
|
||||||
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||||
.map { Signature(it.data) }.toTypedArray()
|
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
|
||||||
|
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||||
|
.map { Signature(it.data) }.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun verify(): Boolean {
|
fun verify(): Boolean {
|
||||||
val res = ApkVerifier.Builder(apk)
|
val res = ApkVerifier.Builder(apk)
|
||||||
.build()
|
.build()
|
||||||
.verify()
|
.verify()
|
||||||
|
|
||||||
return res.isVerified
|
return res.isVerified
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ data class InstalledPackage(val root: File) {
|
|||||||
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
|
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
|
||||||
|
|
||||||
ImageIO.write(read, "png", icon)
|
ImageIO.write(read, "png", icon)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
icon.delete()
|
icon.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,7 @@ data class InstalledPackage(val root: File) {
|
|||||||
fun writeJar() {
|
fun writeJar() {
|
||||||
try {
|
try {
|
||||||
Dex2jar.from(apk).to(jar.toPath())
|
Dex2jar.from(apk).to(jar.toPath())
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
jar.delete()
|
jar.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,4 +94,4 @@ data class InstalledPackage(val root: File) {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class PackageController {
|
|||||||
if (!installed.jar.exists()) {
|
if (!installed.jar.exists()) {
|
||||||
throw IllegalStateException("Failed to translate APK dex!")
|
throw IllegalStateException("Failed to translate APK dex!")
|
||||||
}
|
}
|
||||||
} catch(t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
root.deleteRecursively()
|
root.deleteRecursively()
|
||||||
throw t
|
throw t
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ class PackageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deletePackage(pack: InstalledPackage) {
|
fun deletePackage(pack: InstalledPackage) {
|
||||||
if(!pack.root.exists()) error("Package was never installed!")
|
if (!pack.root.exists()) error("Package was never installed!")
|
||||||
|
|
||||||
val packageName = pack.info.packageName
|
val packageName = pack.info.packageName
|
||||||
pack.root.deleteRecursively()
|
pack.root.deleteRecursively()
|
||||||
@@ -74,7 +74,7 @@ class PackageController {
|
|||||||
|
|
||||||
fun findPackage(packageName: String): InstalledPackage? {
|
fun findPackage(packageName: String): InstalledPackage? {
|
||||||
val file = File(androidFiles.packagesDir, packageName)
|
val file = File(androidFiles.packagesDir, packageName)
|
||||||
return if(file.exists())
|
return if (file.exists())
|
||||||
InstalledPackage(file)
|
InstalledPackage(file)
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
@@ -84,4 +84,4 @@ class PackageController {
|
|||||||
val pkgName = ApkParsers.getMetaInfo(apkFile).packageName
|
val pkgName = ApkParsers.getMetaInfo(apkFile).packageName
|
||||||
return findPackage(pkgName)?.jar
|
return findPackage(pkgName)?.jar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
|
|||||||
sourceDir = apk.absolutePath
|
sourceDir = apk.absolutePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ interface Resource {
|
|||||||
fun getType(): Class<out Resource>
|
fun getType(): Class<out Resource>
|
||||||
|
|
||||||
fun getValue(): Any?
|
fun getValue(): Any?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ServiceSupport {
|
|||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun startService(context: Context, intent: Intent) {
|
fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
|
||||||
val name = intentToClassName(intent)
|
val name = intentToClassName(intent)
|
||||||
|
|
||||||
logger.debug { "Starting service: $name" }
|
logger.debug { "Starting service: $name" }
|
||||||
@@ -27,15 +27,15 @@ class ServiceSupport {
|
|||||||
|
|
||||||
runningServices[name] = service
|
runningServices[name] = service
|
||||||
|
|
||||||
//Setup service
|
// Setup service
|
||||||
thread {
|
thread {
|
||||||
callOnCreate(service)
|
callOnCreate(service)
|
||||||
//TODO Handle more complex cases
|
// TODO Handle more complex cases
|
||||||
service.onStartCommand(intent, 0, 0)
|
service.onStartCommand(intent, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopService(context: Context, intent: Intent) {
|
fun stopService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
|
||||||
val name = intentToClassName(intent)
|
val name = intentToClassName(intent)
|
||||||
stopService(name)
|
stopService(name)
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ class ServiceSupport {
|
|||||||
fun stopService(name: String) {
|
fun stopService(name: String) {
|
||||||
logger.debug { "Stopping service: $name" }
|
logger.debug { "Stopping service: $name" }
|
||||||
val service = runningServices.remove(name)
|
val service = runningServices.remove(name)
|
||||||
if(service == null) {
|
if (service == null) {
|
||||||
logger.warn { "An attempt was made to stop a service that is not running: $name" }
|
logger.warn { "An attempt was made to stop a service that is not running: $name" }
|
||||||
} else {
|
} else {
|
||||||
thread {
|
thread {
|
||||||
@@ -63,6 +63,6 @@ class ServiceSupport {
|
|||||||
fun serviceInstanceFromClass(className: String): Service {
|
fun serviceInstanceFromClass(className: String): Service {
|
||||||
val clazzObj = Class.forName(className)
|
val clazzObj = Class.forName(className)
|
||||||
return clazzObj.getDeclaredConstructor().newInstance() as? Service
|
return clazzObj.getDeclaredConstructor().newInstance() as? Service
|
||||||
?: throw IllegalArgumentException("$className is not a Service!")
|
?: throw IllegalArgumentException("$className is not a Service!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ object KodeinGlobalHelper {
|
|||||||
* Get a dependency
|
* Get a dependency
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
|
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
|
||||||
return when(type) {
|
return when (type) {
|
||||||
AndroidFiles::class.java -> {
|
AndroidFiles::class.java -> {
|
||||||
val instance: AndroidFiles by (kodein ?: kodein()).instance()
|
val instance: AndroidFiles by (kodein ?: kodein()).instance()
|
||||||
instance as T
|
instance as T
|
||||||
@@ -63,5 +64,4 @@ object KodeinGlobalHelper {
|
|||||||
fun <T : Any> instance(type: Class<T>): T {
|
fun <T : Any> instance(type: Class<T>): T {
|
||||||
return instance(type, null)
|
return instance(type, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,12 @@
|
|||||||
|
# AndroidComapt Root dir
|
||||||
|
androidcompat.rootDir = androidcompat-root
|
||||||
|
|
||||||
# Allow/disallow preference changes (useful for demos)
|
####################### `android.files` (FilesConfigModule) #######################
|
||||||
ts.server.allowConfigChanges = true
|
|
||||||
|
|
||||||
# Enable the WebUI? Note: The API and multi-user sync server ui will remain available even if the WebUI is disabled
|
|
||||||
ts.server.enableWebUi = true
|
|
||||||
|
|
||||||
# 'true' to use the old, buggy/memory-leaking WebUI
|
|
||||||
ts.server.useOldWebUi = false
|
|
||||||
|
|
||||||
# 'true' to pretty print all JSON API responses
|
|
||||||
ts.server.prettyPrintApi = false
|
|
||||||
|
|
||||||
# List of blacklisted/whitelisted API endpoints/operation IDs
|
|
||||||
ts.server.disabledApiEndpoints = []
|
|
||||||
ts.server.enabledApiEndpoints = []
|
|
||||||
|
|
||||||
# Message to print in the console when the API has finished booting
|
|
||||||
ts.server.httpInitializedPrintMessage = ""
|
|
||||||
|
|
||||||
# Use external folder for static files
|
|
||||||
ts.server.useExternalStaticFiles = false
|
|
||||||
ts.server.externalStaticFilesFolder = ""
|
|
||||||
|
|
||||||
# Root storage dir
|
|
||||||
ts.server.rootDir = tachiserver-data
|
|
||||||
# Dir to store JVM patches
|
|
||||||
ts.server.patchesDir = ${ts.server.rootDir}/patches
|
|
||||||
|
|
||||||
# Storage dir for the emulated Android app
|
# Storage dir for the emulated Android app
|
||||||
android.files.rootDir = ${ts.server.rootDir}/android-compat/appdata
|
android.files.rootDir = ${androidcompat.rootDir}/appdata
|
||||||
# External storage dir for the emulated Android app's
|
# External storage dir for the emulated Android app's
|
||||||
android.files.externalStorageDir = ${ts.server.rootDir}/android-compat/extappdata
|
android.files.externalStorageDir = ${androidcompat.rootDir}/extappdata
|
||||||
|
|
||||||
# Internal Android directories
|
# Internal Android directories
|
||||||
android.files.dataDir = ${android.files.rootDir}/data
|
android.files.dataDir = ${android.files.rootDir}/data
|
||||||
@@ -48,37 +24,16 @@ android.files.externalCacheDirs = [${android.files.externalStorageDir}/cache]
|
|||||||
android.files.externalMediaDirs = [${android.files.externalStorageDir}/media]
|
android.files.externalMediaDirs = [${android.files.externalStorageDir}/media]
|
||||||
android.files.downloadCacheDir = ${android.files.externalStorageDir}/downloadCache
|
android.files.downloadCacheDir = ${android.files.externalStorageDir}/downloadCache
|
||||||
|
|
||||||
android.files.packageDir = ${ts.server.rootDir}/android-compat/packages
|
android.files.packageDir = ${androidcompat.rootDir}/android-compat/packages
|
||||||
|
|
||||||
|
####################### `android.app` (ApplicationInfoConfigModule) #######################
|
||||||
|
|
||||||
# Emulated Android app package name
|
# Emulated Android app package name
|
||||||
android.app.packageName = eu.kanade.tachiyomi
|
android.app.packageName = eu.kanade.tachiyomi
|
||||||
# Debug mode for the emulated Android app
|
# Debug mode for the emulated Android app
|
||||||
android.app.debug = true
|
android.app.debug = true
|
||||||
|
|
||||||
|
####################### `android.system` (SystemConfigModule) #######################
|
||||||
|
|
||||||
# Whether or not the emulated Android system is debuggable
|
# Whether or not the emulated Android system is debuggable
|
||||||
android.system.isDebuggable = true
|
android.system.isDebuggable = true
|
||||||
|
|
||||||
# Is the multi-user sync server enabled? Does not affect the single-user sync server included in the API.
|
|
||||||
ts.syncd.enable = false
|
|
||||||
|
|
||||||
# The URL of this server (displayed in the sync server web ui)
|
|
||||||
ts.syncd.baseUrl = "http://example.com"
|
|
||||||
|
|
||||||
# 'true' to disable the API and only enable the multi-user sync server
|
|
||||||
ts.syncd.syncOnlyMode = false
|
|
||||||
|
|
||||||
# The root directory to store synchronized data
|
|
||||||
ts.syncd.rootDir = ${ts.server.rootDir}/sync/accounts
|
|
||||||
|
|
||||||
# Location to store config files for the sandbox
|
|
||||||
ts.syncd.sandboxedConfig = ${ts.server.rootDir}/sync/sandboxed_config.config
|
|
||||||
|
|
||||||
# Recaptcha stuff for signup/login
|
|
||||||
ts.syncd.recaptcha.siteKey = ""
|
|
||||||
ts.syncd.recaptcha.secret = ""
|
|
||||||
|
|
||||||
# Sync server display name
|
|
||||||
ts.syncd.name = "Tachiyomi sync server"
|
|
||||||
|
|
||||||
# Header used to forward the IP to the multi-user sync server if the server is behind a reverse proxy
|
|
||||||
ts.syncd.ipHeader = ""
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Server: v0.X.Y-next + WebUI: rXXX
|
||||||
|
## TL;DR
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
## Tachidesk-Server Changelog
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
## Tachidesk-WebUI Changelog
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
|
||||||
+387
@@ -0,0 +1,387 @@
|
|||||||
|
# Server: v0.6.0 + WebUI: r893
|
||||||
|
## TL;DR
|
||||||
|
- WebUI design went through a whole lot of changes, including
|
||||||
|
- Got rid of hamburger menu, now we have a custom mobile navbar
|
||||||
|
- Unread and Download count badges
|
||||||
|
- Back button so better electron experience
|
||||||
|
- There's a whole lot more that I'm too lazy to explore.
|
||||||
|
- Completely removed anime support
|
||||||
|
- Fixed category reordering
|
||||||
|
- Added support for search filters(Server side only)
|
||||||
|
- Added support for updating library(Server side only)
|
||||||
|
- A bunch of API breaking changes(hence why bumping to v0.6.0)!
|
||||||
|
|
||||||
|
## Tachidesk-Server Changelog
|
||||||
|
- (r996) cleanup (by @AriaMoradi)
|
||||||
|
- (r999) better cleaning algorithm (by @AriaMoradi)
|
||||||
|
- (r1007) remove anime support (by @AriaMoradi)
|
||||||
|
- (r1009) Fix tests ([#226](https://github.com/Suwayomi/Tachidesk-Server/pull/226) by @ntbm)
|
||||||
|
- (r1010) Expose unread and download count of Manga in category api ([#227](https://github.com/Suwayomi/Tachidesk-Server/pull/227) by @ntbm)
|
||||||
|
- (r1011) add Cache Header to Thumbnail Response for improved library performance ([#228](https://github.com/Suwayomi/Tachidesk-Server/pull/228) by @ntbm)
|
||||||
|
- (r1013) Fix unread and download counts casing ([#230](https://github.com/Suwayomi/Tachidesk-Server/pull/230) by @Syer10)
|
||||||
|
- (r1014) Fix broken test ([#231](https://github.com/Suwayomi/Tachidesk-Server/pull/231) by @ntbm)
|
||||||
|
- (r1016) Fix category reorder Endpoint. Added Test for Category Reorder ([#232](https://github.com/Suwayomi/Tachidesk-Server/pull/232) by @ntbm)
|
||||||
|
- (r1017) change windows bundle names (by @AriaMoradi)
|
||||||
|
- (r1018) improve tests (by @AriaMoradi)
|
||||||
|
- (r1019) allow injecting Sources (by @AriaMoradi)
|
||||||
|
- (r1020) update (by @AriaMoradi)
|
||||||
|
- (r1021) fix credit (by @AriaMoradi)
|
||||||
|
- (r1022) cleanup (by @AriaMoradi)
|
||||||
|
- (r1023) refactor (by @AriaMoradi)
|
||||||
|
- (r1024) refactor (by @AriaMoradi)
|
||||||
|
- (r1025) implement Source Filters (by @AriaMoradi)
|
||||||
|
- (r1026) ignore build artifacts generated by teting (by @AriaMoradi)
|
||||||
|
- (r1027) convert request type (by @AriaMoradi)
|
||||||
|
- (r1028) Update CONTRIBUTING.md (by @AriaMoradi)
|
||||||
|
- (r1029) stop supporting zero based image storage ([#242](https://github.com/Suwayomi/src/pull/242) by @AriaMoradi)
|
||||||
|
- (r1030) add manga data to download queue object ([#244](https://github.com/Suwayomi/src/pull/244) by @AriaMoradi)
|
||||||
|
- (r1031) Fix Manga Meta, add Manga Meta test ([#245](https://github.com/Suwayomi/src/pull/245) by @Syer10)
|
||||||
|
- (r1032) add pagination to recentChapters ([#246](https://github.com/Suwayomi/src/pull/246) by @AriaMoradi)
|
||||||
|
- (r1033) update (by @AriaMoradi)
|
||||||
|
- (r1034) Implement Update of Library/Category ([#235](https://github.com/Suwayomi/src/pull/235) by @ntbm)
|
||||||
|
- (r1035) update (by @AriaMoradi)
|
||||||
|
- (r1036) Mention the existence of Mahor's Tachidesk-GTK (by @AriaMoradi)
|
||||||
|
- (r1037) Add a Kotlin DSL for endpoint documentation ([#249](https://github.com/Suwayomi/Tachidesk-Server/pull/249) by @Syer10)
|
||||||
|
- (r1038) update (by @AriaMoradi)
|
||||||
|
- (r1039) update (by @AriaMoradi)
|
||||||
|
- (r1040) cleanup directory names ([#251](https://github.com/Suwayomi/Tachidesk-Server/pull/251) by @AriaMoradi)
|
||||||
|
- (r1041) Fix first page not being detected correctly ([#253](https://github.com/Suwayomi/Tachidesk-Server/pull/253) by @AriaMoradi)
|
||||||
|
- (r1042) Update README.md (by @AriaMoradi)
|
||||||
|
- (r1043) Update README.md (by @AriaMoradi)
|
||||||
|
- (r1044) migrate application directories ([#255](https://github.com/Suwayomi/Tachidesk-Server/pull/255) by @AriaMoradi)
|
||||||
|
- (r1045) add support for MultiSelectListPreference ([#258](https://github.com/Suwayomi/Tachidesk-Server/pull/258) by @AriaMoradi)
|
||||||
|
- (r1046) empty searchTerm support ([#259](https://github.com/Suwayomi/Tachidesk-Server/pull/259) by @AriaMoradi)
|
||||||
|
|
||||||
|
|
||||||
|
## Tachidesk-WebUI
|
||||||
|
- (r821) add Permanent sidebar for desktop widths([#46](https://github.com/Suwayomi/Tachidesk-WebUI/pull/46) by @abhijeetChawla)
|
||||||
|
- (r822) Fix Local Source being missing (by @AriaMoradi)
|
||||||
|
- (r823) fix the ugliness of bare messages (by @AriaMoradi)
|
||||||
|
- (r824) add pull request template (by @AriaMoradi)
|
||||||
|
- (r825) add Unread badges ([#48](https://github.com/Suwayomi/Tachidesk-WebUI/pull/48) by @ntbm)
|
||||||
|
- (r826) Back button implementation ([#47](https://github.com/Suwayomi/Tachidesk-WebUI/pull/47) by @abhijeetChawla)
|
||||||
|
- (r827) remove redundant '/manga' prefix from paths (by @AriaMoradi)
|
||||||
|
- (r828) refactor (by @AriaMoradi)
|
||||||
|
- (r829) put Sources and Extensions in the same screen (by @AriaMoradi)
|
||||||
|
- (r830) Set Fallback Image for broken Thumbnails ([#50](https://github.com/Suwayomi/Tachidesk-WebUI/pull/50) by @ntbm)
|
||||||
|
- (r833) Apply Api changes for unread badges ([#52](https://github.com/Suwayomi/Tachidesk-WebUI/pull/52) by @ntbm)
|
||||||
|
- (r834) add EmptyView to DownloadQueue, refactro strings ([#53](https://github.com/Suwayomi/Tachidesk-WebUI/pull/53) by @abhijeetChawla)
|
||||||
|
- (r835) Bottom navbar for mobile ([#51](https://github.com/Suwayomi/Tachidesk-WebUI/pull/51) by @abhijeetChawla)
|
||||||
|
- (r836) Implement Unread Filter for Library ([#54](https://github.com/Suwayomi/Tachidesk-WebUI/pull/54) by @ntbm)
|
||||||
|
- (r837) fix navbar broken logic (by @AriaMoradi)
|
||||||
|
- (r838) fix navbar (by @AriaMoradi)
|
||||||
|
- (r839) refactor (by @AriaMoradi)
|
||||||
|
- (r840) refactor (by @AriaMoradi)
|
||||||
|
- (r841) refactor (by @AriaMoradi)
|
||||||
|
- (r842) show different NavbarItems depending on device width (by @AriaMoradi)
|
||||||
|
- (r843) remove text decoration (by @AriaMoradi)
|
||||||
|
- (r844) fancy icon based on if path selected (by @AriaMoradi)
|
||||||
|
- (r845) custom Extension icon, google's version is shit (by @AriaMoradi)
|
||||||
|
- (r846) refactor (by @AriaMoradi)
|
||||||
|
- (r848) move info (by @AriaMoradi)
|
||||||
|
- (r849) add Search to Library ([#55](https://github.com/Suwayomi/Tachidesk-WebUI/pull/55) by @ntbm)
|
||||||
|
- (r850) add aspect ratio to the manga card. ([#56](https://github.com/Suwayomi/Tachidesk-WebUI/pull/56) by @abhijeetChawla)
|
||||||
|
- (r851) better wording (by @AriaMoradi)
|
||||||
|
- (r852) reorder nav buttons (by @AriaMoradi)
|
||||||
|
- (r853) nicer gradient (by @AriaMoradi)
|
||||||
|
- (r854) refactor MangaCard (by @AriaMoradi)
|
||||||
|
- (r855) closes #58 (by @AriaMoradi
|
||||||
|
- (r856) Add Resume Reading FAB Manga screen ([#59](https://github.com/Suwayomi/Tachidesk-WebUI/pull/59) by @abhijeetChawla)
|
||||||
|
- (r857) add filter and badge for `downloadCount` ([#62](https://github.com/Suwayomi/Tachidesk-WebUI/pull/62) by @abhijeetChawla)
|
||||||
|
- (r858) add issue template (by @AriaMoradi)
|
||||||
|
- (r859) Change color of navbar in light mode ([#65](https://github.com/Suwayomi/Tachidesk-WebUI/pull/65) by @abhijeetChawla)
|
||||||
|
- (r860) fix manga FAB margins ([#66](https://github.com/Suwayomi/Tachidesk-WebUI/pull/66) by @AriaMoradi)
|
||||||
|
- (r861) remove extra scrollbar on mobile ([#67](https://github.com/Suwayomi/Tachidesk-WebUI/pull/67) by @AriaMoradi)
|
||||||
|
- (r862) Fix Bad messages in Library Appbar search ([#70](https://github.com/Suwayomi/Tachidesk-WebUI/pull/70) by @ntbm)
|
||||||
|
- (r863) ban the style prop (by @AriaMoradi)
|
||||||
|
- (r864) Updates pagination update ([#68](https://github.com/Suwayomi/Tachidesk-WebUI/pull/68) by @AriaMoradi)
|
||||||
|
- (r865) make the whole chapter card into a button ([#73](https://github.com/Suwayomi/Tachidesk-WebUI/pull/73) by @AriaMoradi)
|
||||||
|
- (r866) fix chapter actions not working if manga is not fetched online ([#74](https://github.com/Suwayomi/Tachidesk-WebUI/pull/74) by @AriaMoradi)
|
||||||
|
- (r867) migrate some components to Mui5 new styling system ([#72](https://github.com/Suwayomi/Tachidesk-WebUI/pull/72) by @abhijeetChawla)
|
||||||
|
- (r868) load first page on read manga ([#76](https://github.com/Suwayomi/Tachidesk-WebUI/pull/76) by @AriaMoradi)
|
||||||
|
- (r869) Revert "migrate some components to Mui5 new styling system ([#72](https://github.com/Suwayomi/Tachidesk-WebUI/pull/72))" (by @AriaMoradi)
|
||||||
|
- (r870) migrate Backup to Mui 5 ([#106](https://github.com/Suwayomi/Tachidesk-WebUI/pull/106) by @AriaMoradi)
|
||||||
|
- (r871) migrate EmptyView to Mui 5 ([#95](https://github.com/Suwayomi/Tachidesk-WebUI/pull/95) by @AriaMoradi)
|
||||||
|
- (r872) migrate CategorySelect to Mui 5 ([#85](https://github.com/Suwayomi/Tachidesk-WebUI/pull/85) by @AriaMoradi)
|
||||||
|
- (r873) migrate LibraryOptions to Mui 5 ([#83](https://github.com/Suwayomi/Tachidesk-WebUI/pull/83) by @AriaMoradi)
|
||||||
|
- (r874) migrate ChapterCard.tsx to Mui 5 ([#80](https://github.com/Suwayomi/Tachidesk-WebUI/pull/80) by @AriaMoradi)
|
||||||
|
- (r875) migrate App.tsx to Mui 5 ([#79](https://github.com/Suwayomi/Tachidesk-WebUI/pull/79) by @AriaMoradi)
|
||||||
|
- (r876) migrate SourceConfigure to Mui 5 ([#103](https://github.com/Suwayomi/Tachidesk-WebUI/pull/103) by @AriaMoradi)
|
||||||
|
- (r877) migrate Settings to Mui 5 ([#102](https://github.com/Suwayomi/Tachidesk-WebUI/pull/102) by @AriaMoradi)
|
||||||
|
- (r878) migrate Updates to Mui 5 ([#104](https://github.com/Suwayomi/Tachidesk-WebUI/pull/104) by @AriaMoradi)
|
||||||
|
- (r879) Save tabs number in Url to persist tab when go to other paths ([#78](https://github.com/Suwayomi/Tachidesk-WebUI/pull/78) by @abhijeetChawla)
|
||||||
|
- (r880) migrate LangSelect to Mui 5 ([#86](https://github.com/Suwayomi/Tachidesk-WebUI/pull/86) by @AriaMoradi)
|
||||||
|
- (r881) migrate ExtensionCard.tsx to Mui 5 ([#81](https://github.com/Suwayomi/Tachidesk-WebUI/pull/81) by @AriaMoradi)
|
||||||
|
- (r882) migrate SingleSearch to Mui 5 ([#101](https://github.com/Suwayomi/Tachidesk-WebUI/pull/101) by @AriaMoradi)
|
||||||
|
- (r883) migrate LoadingPlaceholder to Mui 5 ([#96](https://github.com/Suwayomi/Tachidesk-WebUI/pull/96) by @AriaMoradi)
|
||||||
|
- (r884) migrate About to Mui 5 ([#105](https://github.com/Suwayomi/Tachidesk-WebUI/pull/105) by @AriaMoradi)
|
||||||
|
- (r885) migrate SourceCard to Mui 5 ([#82](https://github.com/Suwayomi/Tachidesk-WebUI/pull/82) by @AriaMoradi)
|
||||||
|
- (r886) migrate Manga to Mui 5 ([#99](https://github.com/Suwayomi/Tachidesk-WebUI/pull/99) by @AriaMoradi)
|
||||||
|
- (r887) migrate Browse to Mui 5 ([#98](https://github.com/Suwayomi/Tachidesk-WebUI/pull/98) by @AriaMoradi)
|
||||||
|
- (r888) migrate DesktopSideBar to Mui 5 ([#87](https://github.com/Suwayomi/Tachidesk-WebUI/pull/87) by @AriaMoradi)
|
||||||
|
- (r889) cleanup library ([#107](https://github.com/Suwayomi/Tachidesk-WebUI/pull/107) by @AriaMoradi)
|
||||||
|
- (r890) support for new searchTerm (by @AriaMoradi)
|
||||||
|
- (r891) Revert "support for new searchTerm" (by @AriaMoradi)
|
||||||
|
- (r892) add support for emptySearch ([#109](https://github.com/Suwayomi/Tachidesk-WebUI/pull/109) by @AriaMoradi)
|
||||||
|
- (r893) add support for MultiSelectListPreference ([#108](https://github.com/Suwayomi/Tachidesk-WebUI/pull/108) by @AriaMoradi)
|
||||||
|
|
||||||
|
# Server: v0.5.4 + WebUI: r820
|
||||||
|
## TL;DR
|
||||||
|
- Fixed ReadComicOnline, Toonily and possibly other sources not working
|
||||||
|
- Backup and Restore now includes Updates tab data
|
||||||
|
- Removed Anime support from WebUI, Anime support will also be removed from Tachidesk-Server in a future update
|
||||||
|
|
||||||
|
## Tachidesk-Server Changelog
|
||||||
|
- (r973) convert android.jar lib to a maven repo
|
||||||
|
- (r978) mimic Tachiyomi's behaviour more closely, fixes ReadComicOnline (EN)
|
||||||
|
- (r980) fix export chapter ordering, include new props in backup
|
||||||
|
- (r982) remove isNsfw annotation detection
|
||||||
|
- (r984) use correct time conversion units when doing backups
|
||||||
|
- (r989) Support using a CatalogueSource instead of only HttpSources ([#219](https://github.com/Suwayomi/Tachidesk-Server/pull/219) by @Syer10)
|
||||||
|
- (r991) Use a custom task to run electron ([#220](https://github.com/Suwayomi/Tachidesk-Server/pull/220) by @Syer10)
|
||||||
|
|
||||||
|
## Tachidesk-WebUI Changelog
|
||||||
|
- (r810) fix wrong strings in set Server Address dialog, fixes [#39](https://github.com/Suwayomi/Tachidesk-WebUI/issues/39)
|
||||||
|
- (r811) fix chapterFetch loop
|
||||||
|
- (r812) fix overlapping requests
|
||||||
|
- (r813) fix typo
|
||||||
|
- (r814) Better portrait support ([#41](https://github.com/Suwayomi/Tachidesk-WebUI/issues/41) by @minhe7735)
|
||||||
|
- (r815) fixes Reader navbar colors when in light mode ([#43](https://github.com/Suwayomi/Tachidesk-WebUI/issues/43) by @abhijeetChawla)
|
||||||
|
- (r816) default languages cleanup, force Local source enabled
|
||||||
|
- (r817) force Local source at LangSelect
|
||||||
|
- (r818) rename ExtensionLangSelect: generic name for generic use
|
||||||
|
- (r819) don't show anime anymore
|
||||||
|
- (r820) Remove Anime support
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Server: v0.5.3 + WebUI: r809
|
||||||
|
## TL;DR
|
||||||
|
- added support for a equivalent page to Tachiyomi's Updates tab
|
||||||
|
- fix launchers not working on macOS M1/arm64
|
||||||
|
|
||||||
|
## Tachidesk-Server Changelog
|
||||||
|
- (r956) fix macOS-arm64 bundle launchers not working
|
||||||
|
- (r957) Workaround StdLib issue and add KtLint to all modules ([#206](https://github.com/Suwayomi/Tachidesk-Server/pull/206) by @Syer10)
|
||||||
|
- (r960-r963) Add recently updated chapters(Updates) endpoint
|
||||||
|
|
||||||
|
|
||||||
|
## Tachidesk-WebUI Changelog
|
||||||
|
- (r808) fix chapter list not calling onlineFetch=true
|
||||||
|
- (r809) add support for Updates
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Server: v0.5.2 + WebUI: r807
|
||||||
|
## TL;DR
|
||||||
|
- Fixed Local source not working on Windows
|
||||||
|
- Fixed Chapter numbers being shown incorrectly
|
||||||
|
|
||||||
|
## Tachidesk-Server
|
||||||
|
### Public API
|
||||||
|
#### Non-breaking changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Breaking changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- (r948) Fix ManaToki (KO) and NewToki (KO) (issue [#202](https://github.com/Suwayomi/Tachidesk-Server/issue/202))
|
||||||
|
- (r949) Local source: fix windows paths
|
||||||
|
|
||||||
|
### Private API
|
||||||
|
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-Server/pull/200) by @Syer10)
|
||||||
|
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-Server/pull/199) by @Syer10)
|
||||||
|
|
||||||
|
|
||||||
|
## Tachidesk-WebUI
|
||||||
|
#### Visible changes
|
||||||
|
- (r804) update text positioning on Reader and Player ([#35](https://github.com/Suwayomi/Tachidesk-WebUI/pull/35) by @voltrare)
|
||||||
|
- (r806) Source card for Local source is different
|
||||||
|
- (r807) add Local source guide
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- (r805) fix chapter name
|
||||||
|
|
||||||
|
#### Internal changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Server: v0.5.1 + WebUI: r803
|
||||||
|
## TL;DR
|
||||||
|
- Loading sources' manga list is at least twice as fast
|
||||||
|
- Added support for Tachiyomi's Local source
|
||||||
|
- Added BasicAuth support, now you can protect your Tachidesk instance if you are running it on a public server
|
||||||
|
- Added ability to turn off cache for image requests
|
||||||
|
|
||||||
|
## Tachidesk-Server
|
||||||
|
### Public API
|
||||||
|
#### Non-breaking changes
|
||||||
|
- (r915) add BasicAuth support
|
||||||
|
- (r918) add ability to delete downloaded chapters
|
||||||
|
- (r923-r930) add Local Source
|
||||||
|
- (r938) add ability to turn off cache for image requests
|
||||||
|
|
||||||
|
#### Breaking changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- (r917) detect if a downloaded chapter is missing
|
||||||
|
|
||||||
|
### Private API
|
||||||
|
- (r913) remove expand char limit on MangaTable columns
|
||||||
|
- (r914) migrate to Javalin 4
|
||||||
|
- (r921) depricate zero based chapters
|
||||||
|
- (r937) add ChapterRecognition from tachiyomi, closes #10
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Tachidesk-WebUI
|
||||||
|
#### Visible changes
|
||||||
|
- (r790) nice looking progress percentage
|
||||||
|
- (r791) show a Delete button for downloaded chapters
|
||||||
|
- (r792) Update hover effect using more of Material-UI color pallete ([#29](https://github.com/Suwayomi/Tachidesk-WebUI/pull/29) by @voltrare)
|
||||||
|
- (r793) Optimize images ([#32](https://github.com/Suwayomi/Tachidesk-WebUI/pull/32) by @phanirithvij)
|
||||||
|
- (r794) try fix #30 ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31) by @phanirithvij)
|
||||||
|
- (r795) fix viewing page number when the string is long
|
||||||
|
- (r796) show proper display name for source
|
||||||
|
- (r797) fail gracefully when a thumbnail has errors
|
||||||
|
- (r798) fix when a source fails to load mangas
|
||||||
|
- (r800) add Local source ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31))
|
||||||
|
- (r803) add support for useCache
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Internal changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Server: v0.5.0 + WebUI: r789
|
||||||
|
## TL;DR
|
||||||
|
- You can now install APK extensions from the extensions page
|
||||||
|
- WebUI now comes with an updated Material Design looks and is faster a little bit.
|
||||||
|
- WebUI now shows Nsfw content by default, disable it in settings if you prefer to not see Nsfw stuff
|
||||||
|
- Added support for configuration of sources, this enables MangaDex, Komga, Cubari and many other sources
|
||||||
|
- Chapters in the Manga page and Sources in the source page now look nicer and will glow with mouse hover
|
||||||
|
|
||||||
|
## Tachidesk-Server
|
||||||
|
### Public API
|
||||||
|
#### Non-breaking changes
|
||||||
|
- (r888) add installing APK from external sources endpoint
|
||||||
|
|
||||||
|
#### Breaking changes
|
||||||
|
- (r877 [#188](https://github.com/Suwayomi/Tachidesk-Server/pull/188) by @Syer10) `MangaDataClass.genre` changed type to `List<String>`
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- (r899-r901) fix when an external apk is installed and it doesn't have the default tachiyomi-extensions name
|
||||||
|
- (r905) fix a bug where if two sources return the same URL, a false duplicate might be detected
|
||||||
|
|
||||||
|
### Private API
|
||||||
|
- (r887) the `run` task won't call `downloadWebUI` now
|
||||||
|
- (r902) cleanup print/ln instances
|
||||||
|
- (r906) better handling of uninstalling Extensions
|
||||||
|
|
||||||
|
## Tachidesk-WebUI
|
||||||
|
#### Visible changes
|
||||||
|
- (r770) add support for the new genre type
|
||||||
|
- (r771) set the default value of `showNsfw` to `true` so we won't have visual artifacts with a clean install
|
||||||
|
- (r774 [#21](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @voltrare) `ReaderNavbar.jsx`: Swap close and retract Navbar buttons
|
||||||
|
- (r775 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) `yarn.lock`: Fixes version inconsistency after commit 9b866811b
|
||||||
|
- (r776 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) add margin between Source and Extension cards, make the Search button look nicer
|
||||||
|
- (r777) add support for installing external APK files
|
||||||
|
- (r778) fix the makeToaster?
|
||||||
|
- (r779) Action button for installing external extension
|
||||||
|
- (r780 Suwayomi/Tachidesk-WebUI#25) add on hover, active effect to Chapter/Episode card
|
||||||
|
- (r782-r785) updating material-ui to v5 changed the theme
|
||||||
|
- (r785-r788) better `SourceCard` looks on mobile, move `SourceDataClass.isConfigurable` gear button to `SourceMangas`
|
||||||
|
- (r789) implement source configuration
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Internal changes
|
||||||
|
- (r782-r785) update dependencies, migrate material-ui from v4 to v5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Server: v0.4.9 + WebUI: r769
|
||||||
|
## Tachidesk-Server
|
||||||
|
### Public API
|
||||||
|
#### Non-breaking changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Breaking changes
|
||||||
|
- (r857) renamed: `SourceDataClass.isNSFW` -> `SourceDataClass.isNsfw`
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Private API
|
||||||
|
- (r850) Bump WebUI version to r767
|
||||||
|
- (r861) Bump WebUI version to r769
|
||||||
|
|
||||||
|
#### Non-code changes
|
||||||
|
- (r851) Add this changelog file and `CHANGELOG-TEMPLATE.md`
|
||||||
|
- (r852-r853) `CONTRIBUTING.md`: Add a note about this maintaining this file changelog
|
||||||
|
- (r855) `CONTRIBUTING.md`: text cleanup
|
||||||
|
- (r859) `CONTRIBUTING.md`: remove dumb rule
|
||||||
|
- (r862) `windows-bundler.sh`: update jre
|
||||||
|
- (r864) add linux and macOS bundler script and launcher scripts
|
||||||
|
- (r865) fix macOS bundler script and launcher scripts
|
||||||
|
- (r866) bump electron version to v14.0.0
|
||||||
|
- (r868) add linux and macOS bundlers to the publish workflow
|
||||||
|
- (r871) `publish.yml`: remove node module cache, won't need it anymore
|
||||||
|
- (r873) `publish.yml` and `build_push.yml`: fix oopsies
|
||||||
|
|
||||||
|
|
||||||
|
## Tachidesk-WebUI
|
||||||
|
#### Visible changes
|
||||||
|
- (r767-r769) Support for hiding NSFW content in settings screen, extensions screen, sources screen
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Internal changes
|
||||||
|
- (r767) Remove some duplicate dependency declaration from `package.json`
|
||||||
|
|
||||||
|
#### Non-code changes
|
||||||
|
- (r42-r45) Change `README.md`: some links and stuff
|
||||||
|
- (r45-r765) Add all of the commit history from when WebUI was separated from Server, jumping from r45 to r765 (r45 is exactly the same as r765)
|
||||||
|
- (r766) Steal `.gitattributes` from Tachidesk-Server
|
||||||
|
- (r767) Dependency cleanup in `package.json`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Server: v0.4.8 + WebUI: r41
|
||||||
|
## Tachidesk-Server
|
||||||
|
### Public API
|
||||||
|
#### Non-breaking changes
|
||||||
|
- Added support for serializing Search Filters
|
||||||
|
- `SourceDataClass` now has a `isNsfw` key
|
||||||
|
|
||||||
|
#### Breaking changes
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- Fixed a bug where backup restore reversed chapter order
|
||||||
|
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
|
||||||
|
|
||||||
|
### Private API
|
||||||
|
- Added `CloudflareInterceptor` from TachiWeb-Server
|
||||||
|
- Restoring backup for mangas in library(merging manga data) is now supported
|
||||||
|
|
||||||
|
## Tachidesk-WebUI
|
||||||
|
#### Visible changes
|
||||||
|
- Better looking manga card titles
|
||||||
|
- Better reader title, next, prev buttons
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
|
||||||
|
- Re-ordering categories now works
|
||||||
|
|
||||||
|
#### Internal changes
|
||||||
|
- N/A
|
||||||
+41
-37
@@ -2,51 +2,55 @@
|
|||||||
## Where should I start?
|
## Where should I start?
|
||||||
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
||||||
|
|
||||||
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
**Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||||
|
|
||||||
## How does Tachidesk work?
|
**Note 2:** Your pull request will be squashed into a single commit.
|
||||||
|
|
||||||
|
### Project goals and vision
|
||||||
|
- Porting Tachiyomi and covering it's features
|
||||||
|
- Syncing with Tachiyomi, [main issue](https://github.com/Suwayomi/Tachidesk-Server/issues/159)
|
||||||
|
- Generally rejecting features that Tachiyomi(main app) doesn't have,
|
||||||
|
- Unless it's something that makes sense for desktop sizes or desktop form factor (keyboard + mouse)
|
||||||
|
- Additional/crazy features can go in forks and alternative clients
|
||||||
|
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) should
|
||||||
|
- be responsive
|
||||||
|
- support both desktop and mobile form factors well
|
||||||
|
|
||||||
|
## How does Tachidesk-Server work?
|
||||||
This project has two components:
|
This project has two components:
|
||||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
1. **Server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a REST API.
|
||||||
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
2. **WebUI:** A React SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Tachidesk-WebUI
|
||||||
|
|
||||||
## Why a web app?
|
## Why a web app?
|
||||||
This structure is chosen to
|
This structure is chosen to
|
||||||
- Achieve the maximum multi-platform-ness
|
- Achieve the maximum multi-platform-ness
|
||||||
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
|
- Gives the ability to access Tachidesk-Server from a remote client e.g., your phone, tablet or smart TV
|
||||||
- Eaise development of alternative user intefaces for Tachidesk
|
- Ease development of user interfaces for Tachidesk
|
||||||
|
|
||||||
## User Interfaces for Tachidesk server
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
You need these software packages installed in order to build the project
|
You need these software packages installed in order to build the project
|
||||||
### Server
|
|
||||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
|
||||||
- Android stubs jar
|
|
||||||
- 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.
|
|
||||||
### webUI
|
|
||||||
- Nodejs LTS or latest
|
|
||||||
- Yarn
|
|
||||||
- Git
|
|
||||||
### building the full-blown jar
|
|
||||||
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
|
||||||
### 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`.
|
|
||||||
### building the Windows package
|
|
||||||
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
|
|
||||||
First satisfy [the prerequisites](#prerequisites)
|
|
||||||
### server
|
|
||||||
run `./gradlew :server:run --stacktrace` to run the server
|
|
||||||
### webUI
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
and supports HMR and all the other goodies you'll need.
|
|
||||||
|
|
||||||
|
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||||
|
|
||||||
|
### building the full-blown jar (Tachidesk-Server + Tachidesk-WebUI bundle)
|
||||||
|
Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
|
||||||
|
|
||||||
|
### building without `webUI` bundled (server only)
|
||||||
|
Delete `server/src/main/resources/WebUI.zip` if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
|
||||||
|
|
||||||
|
### building the Windows package
|
||||||
|
First Build the jar, then cd into the `scripts` directory and run `./windows-bundler.sh win32` or `./windows-bundler.sh win64` depending on the target architecture, the resulting built zip package file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx-winXX.zip`.
|
||||||
|
|
||||||
|
## Running in development mode
|
||||||
|
run `./gradlew :server:run --stacktrace` to run the server
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
run `./gradlew :server:test` to execute all tests
|
||||||
|
to test a specific class run `./gradlew :server:test --tests <package.with.classname>`
|
||||||
|
|
||||||
|
## Building the android-jar maven repository
|
||||||
|
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,
|
||||||
|
then use `AndroidCompat/lib/android.jar` to manually create a maven repository inside the `android-jar` git branch.
|
||||||
|
Update the dependency declaration afterwards.
|
||||||
|
|||||||
@@ -3,56 +3,111 @@
|
|||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
# Tachidesk
|
## Table of Content
|
||||||
|
- [What is Tachidesk?](#what-is-tachidesk)
|
||||||
|
- [Tachidesk client projects](#tachidesk-client-projects)
|
||||||
|
* [Is this application usable? Should I test it?](#is-this-application-usable-should-i-test-it)
|
||||||
|
- [Downloading and Running the app](#downloading-and-running-the-app)
|
||||||
|
* [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
|
||||||
|
- [Launcher Scripts](#launcher-scripts)
|
||||||
|
+ [Windows](#windows)
|
||||||
|
+ [macOS](#macos)
|
||||||
|
+ [GNU/Linux](#gnulinux)
|
||||||
|
* [Other methods of getting Tachidesk](#other-methods-of-getting-tachidesk)
|
||||||
|
+ [Arch Linux](#arch-linux)
|
||||||
|
+ [Ubuntu-based distributions](#ubuntu-based-distributions)
|
||||||
|
+ [Docker](#docker)
|
||||||
|
* [Advanced Methods](#advanced-methods)
|
||||||
|
+ [Running the jar release directly](#running-the-jar-release-directly)
|
||||||
|
+ [Using Tachidesk Remotely](#using-tachidesk-remotely)
|
||||||
|
- [Syncing With Tachiyomi](#syncing-with-tachiyomi)
|
||||||
|
- [Troubleshooting and Support](#troubleshooting-and-support)
|
||||||
|
- [Contributing and Technical info](#contributing-and-technical-info)
|
||||||
|
- [Credit](#credit)
|
||||||
|
- [License](#license)
|
||||||
|
<!-- Generated with https://ecotrust-canada.github.io/markdown-toc/ -->
|
||||||
|
|
||||||
|
# What is 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"/>
|
||||||
|
|
||||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||||
|
|
||||||
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||||
|
|
||||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
`Tachidesk` is a general term used to describe the combination of Tachidesk-Server(this project) and one of our clients.
|
||||||
|
Think of it roughly like the concept of "distribution" in GNU/Linux distributions, in which Linux(Tachidesk-Server) is the kernel and the difference is which desktop environment(Tachidesk client) you get with it.
|
||||||
|
|
||||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
Tachidesk-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||||
|
|
||||||
**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!**
|
Ability to sync with Tachiyomi is a planned feature.
|
||||||
|
|
||||||
|
# Tachidesk client projects
|
||||||
|
**You need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.**
|
||||||
|
|
||||||
|
Here's a list of known clients/user interfaces for Tachidesk-Server:
|
||||||
|
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster.
|
||||||
|
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced. Downlading Tachidesk-JUI is not recommened for now, the current release is getting obsolete, a new version is to be released soon(TM).
|
||||||
|
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
|
||||||
|
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client, in super early stage of development.
|
||||||
|
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development. Seemingly abandoned.
|
||||||
|
|
||||||
## 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:
|
||||||
|
|
||||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources
|
||||||
- A library to save your mangas and categories to put them into.
|
- A library to save your mangas and categories to put them into
|
||||||
- Searching and browsing installed sources.
|
- Searching and browsing installed sources
|
||||||
- A decent chapter reader.
|
|
||||||
- Ability to download Manga for offline read
|
- Ability to download Manga for offline read
|
||||||
- Backup and restore support powered by Tachiyomi Legacy Backups
|
- Backup and restore support powered by Tachiyomi-compatible Backups
|
||||||
|
- Viewing latest updated chapters.
|
||||||
|
|
||||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
|
**Note:** These are capabilities of Tachidesk-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
|
||||||
|
|
||||||
## Downloading and Running the app
|
# Downloading and Running the app
|
||||||
### All Operating Systems
|
## Using Operating System Specific Bundles
|
||||||
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.
|
To facilitate the use of Tachidesk we provide bundle releases that include The Java Runtime Environment, ElectronJS and 3 Tachidesk Launcher Scripts.
|
||||||
|
|
||||||
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).
|
If a bundle for your operating system or cpu architecture is not provided then refer to [Advanced Methods](#advanced-methods)
|
||||||
|
|
||||||
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.
|
#### Launcher Scripts
|
||||||
|
- `Tachidesk Electron Launcher`: Launches Tachidesk inside Electron as a desktop applicaton
|
||||||
|
- `Tachidesk Browser Launcher`: Launches Tachidesk in a browser window
|
||||||
|
- `Tachidesk Debug Launcher`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
|
||||||
|
|
||||||
|
**Node:** Linux launcher scripts are named a bit differently but work the same.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
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).
|
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) 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-win64.zip` and run one of the Launcher files depending on what you want(see bellow). The rest works like the previous section.
|
Unzip the downloaded file and double click on one of the launcher scripts.
|
||||||
#### 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.
|
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) 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).
|
||||||
|
|
||||||
|
Unzip the downloaded file and double click on one of the launcher scripts.
|
||||||
|
|
||||||
|
### GNU/Linux
|
||||||
|
Download the latest `linux-x64`(x86_64) 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).
|
||||||
|
|
||||||
|
`tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal.
|
||||||
|
|
||||||
|
## Other methods of getting Tachidesk
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
You can install Tachidesk from the AUR
|
You can install Tachidesk from the AUR
|
||||||
```
|
```
|
||||||
yay -S tachidesk
|
yay -S tachidesk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Ubuntu-based distributions
|
||||||
|
More information can be found on the [PPA's page](https://launchpad.net/~suwayomi/+archive/ubuntu/tachidesk).
|
||||||
|
```
|
||||||
|
sudo add-apt-repository ppa:suwayomi/tachidesk
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install tachidesk
|
||||||
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
Check our Offical Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) or use [arbuilder's](https://github.com/arbuilder/Tachidesk-docker) tachidesk docker repo for installation. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser.
|
Check our Official Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Tachidesk Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser.
|
||||||
|
|
||||||
Install from the command line:
|
Install from the command line:
|
||||||
```
|
```
|
||||||
@@ -63,8 +118,33 @@ Run Container from the command line:
|
|||||||
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk
|
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Advanced Methods
|
||||||
|
### Running the jar release directly
|
||||||
|
In order to run the app you need the following:
|
||||||
|
- The jar release of Tachidesk-Server
|
||||||
|
- The Java Runtime Environment(JRE) 8 or newer
|
||||||
|
- A Browser like Google Chrome, Firefox, Edge, etc.
|
||||||
|
- ElectronJS (optional)
|
||||||
|
|
||||||
|
Download the latest `.jar` release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||||
|
|
||||||
|
Make sure you have The Java Runtime Environment installed on your system, Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically.
|
||||||
|
|
||||||
### Using Tachidesk Remotely
|
### Using Tachidesk Remotely
|
||||||
You can run Tachidesk on your computer or a server and connect to it remotely through the web interface with a web browser on any device including a mobile or tablet or even your smart TV!, this method of using Tachidesk is only recommended if you are a power user and know what you are doing.
|
You can run Tachidesk on your computer or a server and connect to it remotely through one of our clients or the bundled web interface with a web browser. This method of using Tachidesk is requires a bit of networking/firewall/port forwarding/server configuration/etc. knowledge on your side, if you can run a Minecraft server and configure it, then you are good to go.
|
||||||
|
|
||||||
|
Check out [this wiki page](https://github.com/Suwayomi/Tachidesk-Server/wiki/Configuring-Tachidesk-Server) for a guide on configuring Tachidesk-Server.
|
||||||
|
|
||||||
|
If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click clack away!).
|
||||||
|
|
||||||
|
## Syncing With Tachiyomi
|
||||||
|
### The Tachidesk extension
|
||||||
|
- You can install the `Tachidesk` extension inside tachiyomi.
|
||||||
|
- The extension will load Tachidesk library.
|
||||||
|
- By manipulating filters you can browse your categories.
|
||||||
|
|
||||||
|
### Other methods
|
||||||
|
Checkout [this issue](https://github.com/Suwayomi/Tachidesk-Server/issues/159) for tracking progress.
|
||||||
|
|
||||||
## Troubleshooting and Support
|
## Troubleshooting and Support
|
||||||
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
|
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
|
||||||
@@ -75,9 +155,9 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|||||||
## Credit
|
## Credit
|
||||||
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
||||||
|
|
||||||
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0` and `Copyright 2019 Andy Bao and contributors`.
|
||||||
|
|
||||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0` and `Copyright 2015 Javier Tomás`.
|
||||||
|
|
||||||
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
|||||||
+45
-21
@@ -1,8 +1,12 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.4.32"
|
kotlin("jvm") version kotlinVersion
|
||||||
kotlin("plugin.serialization") version "1.4.32" apply false
|
kotlin("plugin.serialization") version kotlinVersion
|
||||||
|
id("org.jmailen.kotlinter") version "3.6.0"
|
||||||
|
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@@ -12,10 +16,9 @@ allprojects {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://maven.google.com/")
|
google()
|
||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
|
||||||
maven("https://dl.google.com/dl/android/maven2/")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,18 +30,38 @@ val projects = listOf(
|
|||||||
|
|
||||||
configure(projects) {
|
configure(projects) {
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
|
||||||
|
apply(plugin = "org.jmailen.kotlinter")
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks {
|
||||||
kotlinOptions {
|
withType<KotlinCompile> {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
dependsOn(formatKotlin)
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<LintTask> {
|
||||||
|
source(files("src/kotlin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<FormatTask> {
|
||||||
|
source(files("src/kotlin"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
@@ -46,31 +69,30 @@ configure(projects) {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
|
|
||||||
// coroutines
|
// coroutines
|
||||||
val coroutinesVersion = "1.4.3"
|
val coroutinesVersion = "1.5.2"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
|
|
||||||
val kotlinSerializationVersion = "1.1.0"
|
val kotlinSerializationVersion = "1.3.0-RC"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
|
|
||||||
|
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
|
implementation("org.kodein.di:kodein-di-conf-jvm:7.8.0")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
implementation("org.slf4j:slf4j-api:1.7.32")
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
implementation("ch.qos.logback:logback-classic:1.2.6")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
implementation("io.github.microutils:kotlin-logging:2.0.11")
|
||||||
|
|
||||||
// ReactiveX
|
// 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")
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
|
|
||||||
// JSoup
|
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.14.2")
|
||||||
|
|
||||||
// dependency of :AndroidCompat:Config
|
// dependency of :AndroidCompat:Config
|
||||||
implementation("com.typesafe:config:1.4.1")
|
implementation("com.typesafe:config:1.4.1")
|
||||||
@@ -79,13 +101,15 @@ configure(projects) {
|
|||||||
// to get application content root
|
// to get application content root
|
||||||
implementation("net.harawata:appdirs:1.2.1")
|
implementation("net.harawata:appdirs:1.2.1")
|
||||||
|
|
||||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
// dex2jar
|
||||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
val dex2jarVersion = "v26"
|
||||||
|
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
|
||||||
|
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")
|
||||||
|
|
||||||
// APK parser
|
// APK parser
|
||||||
implementation("net.dongliu:apk-parser:2.6.10")
|
implementation("net.dongliu:apk-parser:2.6.10")
|
||||||
|
|
||||||
// Jackson
|
// dependency both in AndroidCompat and server, version locked by javalin
|
||||||
implementation("com.fasterxml.jackson.core:jackson-annotations:2.10.3")
|
implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import java.io.BufferedReader
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
const val kotlinVersion = "1.5.30"
|
||||||
|
|
||||||
|
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||||
|
|
||||||
|
// should be bumped with each stable release
|
||||||
|
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.0"
|
||||||
|
|
||||||
|
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r893"
|
||||||
|
|
||||||
|
// counts commits on the master branch
|
||||||
|
val tachideskRevision = runCatching {
|
||||||
|
System.getenv("ProductRevision") ?: Runtime
|
||||||
|
.getRuntime()
|
||||||
|
.exec("git rev-list HEAD --count")
|
||||||
|
.let { process ->
|
||||||
|
process.waitFor()
|
||||||
|
val output = process.inputStream.use {
|
||||||
|
it.bufferedReader().use(BufferedReader::readText)
|
||||||
|
}
|
||||||
|
process.destroy()
|
||||||
|
"r" + output.trim()
|
||||||
|
}
|
||||||
|
}.getOrDefault("r0")
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
cd "`dirname "$0"`"
|
||||||
|
|
||||||
|
./jre/Contents/Home/bin/java -jar Tachidesk.jar
|
||||||
@@ -1 +1,7 @@
|
|||||||
jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar
|
:: cleaner output
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
|
|
||||||
|
:: Prevent cmd from closing when Tachidesk crashes
|
||||||
|
pause
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
cd "`dirname "$0"`"
|
||||||
|
|
||||||
|
./jre/Contents/Home/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
@@ -1 +1 @@
|
|||||||
jre\bin\javaw "-Dsuwayomi.tachidesk.server.webInterface=electron" "-Dsuwayomi.tachidesk.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
cd "`dirname "$0"`"
|
||||||
|
|
||||||
|
./jre/Contents/Home/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/Electron.app/Contents/MacOS/Electron" -jar Tachidesk.jar
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
./jre/bin/java -jar Tachidesk.jar
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
./jre/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
./jre/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron" -jar Tachidesk.jar
|
||||||
Executable
+89
@@ -0,0 +1,89 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
electron_version="v14.0.0"
|
||||||
|
|
||||||
|
if [ $1 = "linux-x64" ]; then
|
||||||
|
jre="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
|
||||||
|
jre_release="jdk8u302-b08"
|
||||||
|
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
|
||||||
|
jre_dir="$jre_release-jre"
|
||||||
|
electron="electron-$electron_version-linux-x64.zip"
|
||||||
|
elif [ $1 = "macOS-x64" ]; then
|
||||||
|
jre="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
|
||||||
|
jre_release="jdk8u302-b08"
|
||||||
|
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
|
||||||
|
jre_dir="$jre_release-jre"
|
||||||
|
electron="electron-$electron_version-darwin-x64.zip"
|
||||||
|
elif [ $1 = "macOS-arm64" ]; then
|
||||||
|
jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
|
||||||
|
jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
|
||||||
|
jre_url="https://cdn.azul.com/zulu/bin/$jre"
|
||||||
|
jre_dir="$jre_release/zulu-8.jre"
|
||||||
|
electron="electron-$electron_version-darwin-arm64.zip"
|
||||||
|
else
|
||||||
|
echo "Unsupported arch value: $1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
arch="$1"
|
||||||
|
os=$(echo $arch | cut -d '-' -f1)
|
||||||
|
|
||||||
|
echo "creating $arch bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/*.jar | tail -n1)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | sed 's/.jar//')-$arch
|
||||||
|
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
|
||||||
|
echo "Dealing with jre..."
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L $jre_url -o $jre
|
||||||
|
fi
|
||||||
|
tar xvf $jre
|
||||||
|
mv $jre_dir $release_name/jre
|
||||||
|
|
||||||
|
echo "Dealing with electron"
|
||||||
|
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
|
||||||
|
|
||||||
|
# copy artifacts
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
if [ $os = linux ]; then
|
||||||
|
cp "resources/tachidesk-browser-launcher.sh" $release_name
|
||||||
|
cp "resources/tachidesk-debug-launcher.sh" $release_name
|
||||||
|
cp "resources/tachidesk-electron-launcher.sh" $release_name
|
||||||
|
elif [ $os = macOS ]; then
|
||||||
|
cp "resources/Tachidesk Browser Launcher.command" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.command" $release_name
|
||||||
|
cp "resources/Tachidesk Electron Launcher.command" $release_name
|
||||||
|
fi
|
||||||
|
|
||||||
|
archive_name=""
|
||||||
|
if [ $os = linux ]; then
|
||||||
|
archive_name=$release_name.tar.gz
|
||||||
|
GZIP=-9 tar cvzf $archive_name $release_name
|
||||||
|
elif [ $os = macOS ]; then
|
||||||
|
archive_name=$release_name.zip
|
||||||
|
zip -9 -r $archive_name $release_name
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
# clean up from possible previous runs
|
||||||
|
if [ -f ../server/build/$archive_name ]; then
|
||||||
|
rm ../server/build/$archive_name
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv $archive_name ../server/build/
|
||||||
@@ -6,19 +6,23 @@
|
|||||||
# 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/.
|
||||||
|
|
||||||
electron_version="v12.0.9"
|
electron_version="v14.0.0"
|
||||||
|
|
||||||
if [ $1 = "win32" ]; then
|
if [ $1 = "win32" ]; then
|
||||||
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
arch="win32"
|
jre_release="jdk8u292-b10"
|
||||||
|
jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre"
|
||||||
|
arch="windows-x86"
|
||||||
electron="electron-$electron_version-win32-ia32.zip"
|
electron="electron-$electron_version-win32-ia32.zip"
|
||||||
else
|
else
|
||||||
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
|
||||||
arch="win64"
|
jre_release="jdk8u302-b08"
|
||||||
|
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
|
||||||
|
arch="windows-x64"
|
||||||
electron="electron-$electron_version-win32-x64.zip"
|
electron="electron-$electron_version-win32-x64.zip"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jre_dir="jdk8u292-b10-jre"
|
jre_dir="$jre_release-jre"
|
||||||
|
|
||||||
echo "creating windows bundle"
|
echo "creating windows bundle"
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ mkdir $release_name
|
|||||||
|
|
||||||
echo "Dealing with jre..."
|
echo "Dealing with jre..."
|
||||||
if [ ! -f $jre ]; then
|
if [ ! -f $jre ]; then
|
||||||
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
|
curl -L $jre_url -o $jre
|
||||||
fi
|
fi
|
||||||
unzip $jre
|
unzip $jre
|
||||||
mv $jre_dir $release_name/jre
|
mv $jre_dir $release_name/jre
|
||||||
|
|||||||
+81
-100
@@ -1,24 +1,10 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import de.undercouch.gradle.tasks.download.Download
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
plugins {
|
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("com.github.gmazzo.buildconfig")
|
||||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
url = uri("https://repo1.maven.org/maven2/")
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url = uri("https://jitpack.io")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -30,13 +16,15 @@ dependencies {
|
|||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
// Javalin api
|
// Javalin api
|
||||||
implementation("io.javalin:javalin:3.13.6")
|
implementation("io.javalin:javalin:4.1.1")
|
||||||
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
|
implementation("io.javalin:javalin-openapi:4.1.1")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
|
val jacksonVersion = "2.12.4"
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
|
||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposedVersion = "0.31.1"
|
val exposedVersion = "0.34.1"
|
||||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||||
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")
|
||||||
@@ -45,29 +33,32 @@ dependencies {
|
|||||||
implementation("com.h2database:h2:1.4.200")
|
implementation("com.h2database:h2:1.4.200")
|
||||||
|
|
||||||
// Exposed Migrations
|
// Exposed Migrations
|
||||||
val exposedMigrationsVersion = "3.1.0"
|
implementation("com.github.Suwayomi:exposed-migrations:3.1.4")
|
||||||
implementation("com.github.Suwayomi:exposed-migrations:$exposedMigrationsVersion")
|
|
||||||
|
|
||||||
// tray icon
|
// tray icon
|
||||||
implementation("com.dorkbox:SystemTray:4.1")
|
implementation("com.dorkbox:SystemTray:4.1")
|
||||||
implementation("com.dorkbox:Utilities:1.9")
|
implementation("com.dorkbox:Utilities:1.9") // version locked by SystemTray
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.14.2")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
implementation("com.google.code.gson:gson:2.8.8")
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||||
|
|
||||||
// asm for fixing SimpleDateFormat (must match Dex2Jar version)
|
// asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
|
||||||
implementation("org.ow2.asm:asm-debug-all:5.0.3")
|
implementation("org.ow2.asm:asm:9.2")
|
||||||
|
|
||||||
// extracting zip files
|
// Disk & File
|
||||||
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
||||||
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
|
// CloudflareInterceptor
|
||||||
|
implementation("net.sourceforge.htmlunit:htmlunit:2.52.0")
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||||
@@ -79,17 +70,13 @@ dependencies {
|
|||||||
|
|
||||||
// uncomment to test extensions directly
|
// uncomment to test extensions directly
|
||||||
// implementation(fileTree("lib/"))
|
// implementation(fileTree("lib/"))
|
||||||
|
implementation(kotlin("script-runtime"))
|
||||||
|
|
||||||
|
testImplementation("io.mockk:mockk:1.9.3")
|
||||||
}
|
}
|
||||||
|
|
||||||
val MainClass = "suwayomi.tachidesk.MainKt"
|
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
// for testing electron
|
|
||||||
// applicationDefaultJvmArgs = listOf(
|
|
||||||
// "-Dsuwayomi.tachidesk.webInterface=electron",
|
|
||||||
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -100,83 +87,52 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
|
||||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.4"
|
|
||||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r20"
|
|
||||||
|
|
||||||
// counts commit count on master
|
|
||||||
val tachideskRevision = runCatching {
|
|
||||||
System.getenv("ProductRevision") ?: Runtime
|
|
||||||
.getRuntime()
|
|
||||||
.exec("git rev-list HEAD --count")
|
|
||||||
.let { process ->
|
|
||||||
process.waitFor()
|
|
||||||
val output = process.inputStream.use {
|
|
||||||
it.bufferedReader().use(BufferedReader::readText)
|
|
||||||
}
|
|
||||||
process.destroy()
|
|
||||||
"r" + output.trim()
|
|
||||||
}
|
|
||||||
}.getOrDefault("r0")
|
|
||||||
|
|
||||||
buildConfig {
|
buildConfig {
|
||||||
clsName = "BuildConfig"
|
className("BuildConfig")
|
||||||
packageName = "suwayomi.tachidesk.server"
|
packageName("suwayomi.tachidesk.server")
|
||||||
|
|
||||||
|
useKotlinOutput()
|
||||||
|
|
||||||
buildConfigField("String", "NAME", rootProject.name)
|
fun quoteWrap(obj: Any): String = """"$obj""""
|
||||||
buildConfigField("String", "VERSION", tachideskVersion)
|
|
||||||
buildConfigField("String", "REVISION", tachideskRevision)
|
buildConfigField("String", "NAME", quoteWrap(rootProject.name))
|
||||||
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")
|
buildConfigField("String", "VERSION", quoteWrap(tachideskVersion))
|
||||||
|
buildConfigField("String", "REVISION", quoteWrap(tachideskRevision))
|
||||||
|
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
|
||||||
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||||
|
|
||||||
|
|
||||||
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview")
|
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
|
||||||
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag)
|
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
|
||||||
|
|
||||||
|
|
||||||
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk"))
|
||||||
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
shadowJar {
|
shadowJar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
"Main-Class" to MainClass,
|
||||||
"Main-Class" to MainClass,
|
"Implementation-Title" to rootProject.name,
|
||||||
"Implementation-Title" to rootProject.name,
|
"Implementation-Vendor" to "The Suwayomi Project",
|
||||||
"Implementation-Vendor" to "The Suwayomi Project",
|
"Specification-Version" to tachideskVersion,
|
||||||
"Specification-Version" to tachideskVersion,
|
"Implementation-Version" to tachideskRevision
|
||||||
"Implementation-Version" to tachideskRevision
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
archiveBaseName.set(rootProject.name)
|
archiveBaseName.set(rootProject.name)
|
||||||
archiveVersion.set(tachideskVersion)
|
archiveVersion.set(tachideskVersion)
|
||||||
archiveClassifier.set(tachideskRevision)
|
archiveClassifier.set(tachideskRevision)
|
||||||
}
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
withType<KotlinCompile> {
|
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs = listOf(
|
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnit()
|
useJUnitPlatform()
|
||||||
}
|
testLogging {
|
||||||
|
showStandardStreams = true
|
||||||
withType<ShadowJar> {
|
events("passed", "skipped", "failed")
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
}
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
named("run") {
|
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
named<Copy>("processResources") {
|
named<Copy>("processResources") {
|
||||||
@@ -184,16 +140,41 @@ tasks {
|
|||||||
mustRunAfter("downloadWebUI")
|
mustRunAfter("downloadWebUI")
|
||||||
}
|
}
|
||||||
|
|
||||||
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
|
register<Download>("downloadWebUI") {
|
||||||
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
||||||
dest("src/main/resources/WebUI.zip")
|
dest("src/main/resources/WebUI.zip")
|
||||||
|
|
||||||
|
|
||||||
|
fun shouldOverwrite(): Boolean {
|
||||||
|
val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip"
|
||||||
|
val zipFile = net.lingala.zip4j.ZipFile(zipPath)
|
||||||
|
|
||||||
|
var shouldOverwrite = true
|
||||||
|
if (zipFile.isValidZipFile) {
|
||||||
|
val zipRevision = zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use {
|
||||||
|
it.readText().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipRevision == webUIRevisionTag) {
|
||||||
|
shouldOverwrite = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldOverwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
overwrite(shouldOverwrite())
|
||||||
}
|
}
|
||||||
|
|
||||||
withType<LintTask> {
|
register("runElectron") {
|
||||||
source(files("src/kotlin"))
|
group = "application"
|
||||||
}
|
finalizedBy(run)
|
||||||
|
doFirst {
|
||||||
withType<FormatTask> {
|
application.applicationDefaultJvmArgs = listOf(
|
||||||
source(files("src/kotlin"))
|
"-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
|
||||||
|
// Change this to the installed electron application
|
||||||
|
"-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi;
|
||||||
|
|
||||||
|
public class BuildConfig {
|
||||||
|
public static final int VERSION_CODE = -1;
|
||||||
|
public static final String VERSION_NAME = "stub";
|
||||||
|
}
|
||||||
+2
-19
@@ -1,30 +1,13 @@
|
|||||||
/*
|
Copyright 2015 Javier Tomás
|
||||||
Copyright 2014 Prateek Srivastava
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
This file may have been modified after being copied from it's original source.
|
|
||||||
*/
|
|
||||||
package com.f2prateek.rx.preferences;
|
|
||||||
|
|
||||||
final class Preconditions {
|
|
||||||
static void checkNotNull(Object o, String message) {
|
|
||||||
if (o == null) {
|
|
||||||
throw new NullPointerException(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Preconditions() {
|
|
||||||
throw new AssertionError("No instances");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
interface AnimeCatalogueSource : AnimeSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
|
||||||
*/
|
|
||||||
val lang: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the source has support for latest updates.
|
|
||||||
*/
|
|
||||||
val supportsLatest: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of anime.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of anime.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of latest anime updates.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of filters for the source.
|
|
||||||
*/
|
|
||||||
fun getFilterList(): AnimeFilterList
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
|
||||||
*/
|
|
||||||
interface AnimeSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id for the source. Must be unique.
|
|
||||||
*/
|
|
||||||
val id: Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the source.
|
|
||||||
*/
|
|
||||||
val name: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a anime.
|
|
||||||
*
|
|
||||||
* @param anime the anime to update.
|
|
||||||
*/
|
|
||||||
// @Deprecated("Use getAnimeDetails instead")
|
|
||||||
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with all the available episodes for an anime.
|
|
||||||
*
|
|
||||||
* @param anime the anime to update.
|
|
||||||
*/
|
|
||||||
// @Deprecated("Use getEpisodeList instead")
|
|
||||||
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with a link for the episode of an anime.
|
|
||||||
*
|
|
||||||
* @param episode the episode to get the link for.
|
|
||||||
*/
|
|
||||||
// @Deprecated("Use getEpisodeList instead")
|
|
||||||
fun fetchEpisodeLink(episode: SEpisode): Observable<String>
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * [1.x API] Get the updated details for a anime.
|
|
||||||
// */
|
|
||||||
// @Suppress("DEPRECATION")
|
|
||||||
// override suspend fun getAnimeDetails(anime: AnimeInfo): AnimeInfo {
|
|
||||||
// val sAnime = anime.toSAnime()
|
|
||||||
// val networkAnime = fetchAnimeDetails(sAnime).awaitSingle()
|
|
||||||
// sAnime.copyFrom(networkAnime)
|
|
||||||
// return sAnime.toAnimeInfo()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * [1.x API] Get all the available episodes for a anime.
|
|
||||||
// */
|
|
||||||
// @Suppress("DEPRECATION")
|
|
||||||
// override suspend fun getEpisodeList(anime: AnimeInfo): List<EpisodeInfo> {
|
|
||||||
// return fetchEpisodeList(anime.toSAnime()).awaitSingle()
|
|
||||||
// .map { it.toEpisodeInfo() }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * [1.x API] Get a link for the episode of an anime.
|
|
||||||
// */
|
|
||||||
// @Suppress("DEPRECATION")
|
|
||||||
// override suspend fun getEpisodeLink(episode: EpisodeInfo): String {
|
|
||||||
// return fetchEpisodeLink(episode.toSEpisode()).awaitSingle()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this)
|
|
||||||
|
|
||||||
// fun AnimeSource.getPreferenceKey(): String = "source_$id"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A factory for creating sources at runtime.
|
|
||||||
*/
|
|
||||||
interface AnimeSourceFactory {
|
|
||||||
/**
|
|
||||||
* Create a new copy of the sources
|
|
||||||
* @return The created sources
|
|
||||||
*/
|
|
||||||
fun createSources(): List<AnimeSource>
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
open class AnimeSourceManager(private val context: Context) {
|
|
||||||
|
|
||||||
private val sourcesMap = mutableMapOf<Long, AnimeSource>()
|
|
||||||
|
|
||||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
createInternalSources().forEach { registerSource(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun get(sourceKey: Long): AnimeSource? {
|
|
||||||
return sourcesMap[sourceKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOrStub(sourceKey: Long): AnimeSource {
|
|
||||||
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
|
||||||
StubSource(sourceKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<AnimeHttpSource>()
|
|
||||||
|
|
||||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<AnimeCatalogueSource>()
|
|
||||||
|
|
||||||
internal fun registerSource(source: AnimeSource) {
|
|
||||||
if (!sourcesMap.containsKey(source.id)) {
|
|
||||||
sourcesMap[source.id] = source
|
|
||||||
}
|
|
||||||
if (!stubSourcesMap.containsKey(source.id)) {
|
|
||||||
stubSourcesMap[source.id] = StubSource(source.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun unregisterSource(source: AnimeSource) {
|
|
||||||
sourcesMap.remove(source.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createInternalSources(): List<AnimeSource> = listOf(
|
|
||||||
// LocalAnimeSource(context)
|
|
||||||
)
|
|
||||||
|
|
||||||
inner class StubSource(override val id: Long) : AnimeSource {
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = id.toString()
|
|
||||||
|
|
||||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
|
||||||
return Observable.error(getSourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
|
||||||
return Observable.error(getSourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
|
|
||||||
return Observable.error(getSourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSourceNotInstalledException(): Exception {
|
|
||||||
// return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
|
||||||
return Exception("source not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource
|
|
||||||
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
|
|
||||||
interface ConfigurableAnimeSource : AnimeSource {
|
|
||||||
|
|
||||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
sealed class AnimeFilter<T>(val name: String, var state: T) {
|
|
||||||
open class Header(name: String) : AnimeFilter<Any>(name, 0)
|
|
||||||
open class Separator(name: String = "") : AnimeFilter<Any>(name, 0)
|
|
||||||
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : AnimeFilter<Int>(name, state)
|
|
||||||
abstract class Text(name: String, state: String = "") : AnimeFilter<String>(name, state)
|
|
||||||
abstract class CheckBox(name: String, state: Boolean = false) : AnimeFilter<Boolean>(name, state)
|
|
||||||
abstract class TriState(name: String, state: Int = STATE_IGNORE) : AnimeFilter<Int>(name, state) {
|
|
||||||
fun isIgnored() = state == STATE_IGNORE
|
|
||||||
fun isIncluded() = state == STATE_INCLUDE
|
|
||||||
fun isExcluded() = state == STATE_EXCLUDE
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val STATE_IGNORE = 0
|
|
||||||
const val STATE_INCLUDE = 1
|
|
||||||
const val STATE_EXCLUDE = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class Group<V>(name: String, state: List<V>) : AnimeFilter<List<V>>(name, state)
|
|
||||||
|
|
||||||
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
|
|
||||||
AnimeFilter<Sort.Selection?>(name, state) {
|
|
||||||
data class Selection(val index: Int, val ascending: Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is AnimeFilter<*>) return false
|
|
||||||
|
|
||||||
return name == other.name && state == other.state
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = name.hashCode()
|
|
||||||
result = 31 * result + (state?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
data class AnimeFilterList(val list: List<AnimeFilter<*>>) : List<AnimeFilter<*>> by list {
|
|
||||||
|
|
||||||
constructor(vararg fs: AnimeFilter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
data class AnimesPage(val animes: List<SAnime>, val hasNextPage: Boolean)
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface SAnime : Serializable {
|
|
||||||
|
|
||||||
var url: String
|
|
||||||
|
|
||||||
var title: String
|
|
||||||
|
|
||||||
var artist: String?
|
|
||||||
|
|
||||||
var author: String?
|
|
||||||
|
|
||||||
var description: String?
|
|
||||||
|
|
||||||
var genre: String?
|
|
||||||
|
|
||||||
var status: Int
|
|
||||||
|
|
||||||
var thumbnail_url: String?
|
|
||||||
|
|
||||||
var initialized: Boolean
|
|
||||||
|
|
||||||
fun copyFrom(other: SAnime) {
|
|
||||||
if (other.title != null) {
|
|
||||||
title = other.title
|
|
||||||
}
|
|
||||||
|
|
||||||
if (other.author != null) {
|
|
||||||
author = other.author
|
|
||||||
}
|
|
||||||
|
|
||||||
if (other.artist != null) {
|
|
||||||
artist = other.artist
|
|
||||||
}
|
|
||||||
|
|
||||||
if (other.description != null) {
|
|
||||||
description = other.description
|
|
||||||
}
|
|
||||||
|
|
||||||
if (other.genre != null) {
|
|
||||||
genre = other.genre
|
|
||||||
}
|
|
||||||
|
|
||||||
if (other.thumbnail_url != null) {
|
|
||||||
thumbnail_url = other.thumbnail_url
|
|
||||||
}
|
|
||||||
|
|
||||||
status = other.status
|
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
initialized = other.initialized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val UNKNOWN = 0
|
|
||||||
const val ONGOING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val LICENSED = 3
|
|
||||||
|
|
||||||
fun create(): SAnime {
|
|
||||||
return SAnimeImpl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
class SAnimeImpl : SAnime {
|
|
||||||
|
|
||||||
override lateinit var url: String
|
|
||||||
|
|
||||||
override lateinit var title: String
|
|
||||||
|
|
||||||
override var artist: String? = null
|
|
||||||
|
|
||||||
override var author: String? = null
|
|
||||||
|
|
||||||
override var description: String? = null
|
|
||||||
|
|
||||||
override var genre: String? = null
|
|
||||||
|
|
||||||
override var status: Int = 0
|
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface SEpisode : Serializable {
|
|
||||||
|
|
||||||
var url: String
|
|
||||||
|
|
||||||
var name: String
|
|
||||||
|
|
||||||
var date_upload: Long
|
|
||||||
|
|
||||||
var episode_number: Float
|
|
||||||
|
|
||||||
var scanlator: String?
|
|
||||||
|
|
||||||
fun copyFrom(other: SEpisode) {
|
|
||||||
name = other.name
|
|
||||||
url = other.url
|
|
||||||
date_upload = other.date_upload
|
|
||||||
episode_number = other.episode_number
|
|
||||||
scanlator = other.scanlator
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(): SEpisode {
|
|
||||||
return SEpisodeImpl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.model
|
|
||||||
|
|
||||||
class SEpisodeImpl : SEpisode {
|
|
||||||
|
|
||||||
override lateinit var url: String
|
|
||||||
|
|
||||||
override lateinit var name: String
|
|
||||||
|
|
||||||
override var date_upload: Long = 0
|
|
||||||
|
|
||||||
override var episode_number: Float = -1f
|
|
||||||
|
|
||||||
override var scanlator: String? = null
|
|
||||||
}
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.online
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URISyntaxException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple implementation for sources from a website.
|
|
||||||
*/
|
|
||||||
abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Network service.
|
|
||||||
*/
|
|
||||||
protected val network: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Preferences that a source may need.
|
|
||||||
// */
|
|
||||||
// val preferences: SharedPreferences by lazy {
|
|
||||||
// Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
|
||||||
*/
|
|
||||||
abstract val baseUrl: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version id used to generate the source id. If the site completely changes and urls are
|
|
||||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
|
||||||
*/
|
|
||||||
open val versionId = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
|
||||||
* of the MD5 of the string: sourcename/language/versionId
|
|
||||||
* Note the generated id sets the sign bit to 0.
|
|
||||||
*/
|
|
||||||
override val id by lazy {
|
|
||||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Headers used for requests.
|
|
||||||
*/
|
|
||||||
val headers: Headers by lazy { headersBuilder().build() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default network client for doing requests.
|
|
||||||
*/
|
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = network.client
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
|
||||||
*/
|
|
||||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("User-Agent", DEFAULT_USER_AGENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visible name of the source.
|
|
||||||
*/
|
|
||||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> {
|
|
||||||
return client.newCall(popularAnimeRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
popularAnimeParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the popular anime given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
protected abstract fun popularAnimeRequest(page: Int): Request
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [AnimesPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun popularAnimeParse(response: Response): AnimesPage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
|
||||||
return client.newCall(searchAnimeRequest(page, query, filters))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
searchAnimeParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the search anime given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [AnimesPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun searchAnimeParse(response: Response): AnimesPage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of latest anime updates.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<AnimesPage> {
|
|
||||||
return client.newCall(latestUpdatesRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
latestUpdatesParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for latest anime given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
protected abstract fun latestUpdatesRequest(page: Int): Request
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [AnimesPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a anime. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param anime the anime to be updated.
|
|
||||||
*/
|
|
||||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
|
||||||
return client.newCall(animeDetailsRequest(anime))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
animeDetailsParse(response).apply { initialized = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the details of a anime. Override only if it's needed to change the
|
|
||||||
* url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param anime the anime to be updated.
|
|
||||||
*/
|
|
||||||
open fun animeDetailsRequest(anime: SAnime): Request {
|
|
||||||
return GET(baseUrl + anime.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the details of a anime.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun animeDetailsParse(response: Response): SAnime
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated episode list for a anime. Normally it's not needed to
|
|
||||||
* override this method. If a anime is licensed an empty episode list observable is returned
|
|
||||||
*
|
|
||||||
* @param anime the anime to look for episodes.
|
|
||||||
*/
|
|
||||||
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
|
||||||
return if (anime.status != SAnime.LICENSED) {
|
|
||||||
client.newCall(episodeListRequest(anime))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
episodeListParse(response)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Observable.error(Exception("Licensed - No episodes to show"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
|
|
||||||
return client.newCall(episodeLinkRequest(episode))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
episodeLinkParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for updating the episode list. Override only if it's needed to override
|
|
||||||
* the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param anime the anime to look for episodes.
|
|
||||||
*/
|
|
||||||
protected open fun episodeListRequest(anime: SAnime): Request {
|
|
||||||
return GET(baseUrl + anime.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the episode link. Override only if it's needed to override
|
|
||||||
* the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param episode the episode to look for links.
|
|
||||||
*/
|
|
||||||
protected open fun episodeLinkRequest(episode: SEpisode): Request {
|
|
||||||
return GET(baseUrl + episode.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of episodes.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun episodeListParse(response: Response): List<SEpisode>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of episodes.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun episodeLinkParse(response: Response): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
|
||||||
* url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param episode the episode whose page list has to be fetched.
|
|
||||||
*/
|
|
||||||
protected open fun pageListRequest(episode: SEpisode): Request {
|
|
||||||
return GET(baseUrl + episode.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of pages.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun pageListParse(response: Response): List<Page>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the page containing the source url of the image. If there's any
|
|
||||||
* error, it will return null instead of throwing an exception.
|
|
||||||
*
|
|
||||||
* @param page the page whose source image has to be fetched.
|
|
||||||
*/
|
|
||||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
|
||||||
return client.newCall(imageUrlRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { imageUrlParse(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
|
||||||
* override the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param page the episode whose page list has to be fetched
|
|
||||||
*/
|
|
||||||
protected open fun imageUrlRequest(page: Page): Request {
|
|
||||||
return GET(page.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the absolute url to the source image.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
protected abstract fun imageUrlParse(response: Response): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the response of the source image.
|
|
||||||
*
|
|
||||||
* @param page the page whose source image has to be downloaded.
|
|
||||||
*/
|
|
||||||
fun fetchImage(page: Page): Observable<Response> {
|
|
||||||
return client.newCallWithProgress(imageRequest(page), page)
|
|
||||||
.asObservableSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the source image. Override only if it's needed to override
|
|
||||||
* the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param page the episode whose page list has to be fetched
|
|
||||||
*/
|
|
||||||
protected open fun imageRequest(page: Page): Request {
|
|
||||||
return GET(page.imageUrl!!, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns the url of the episode without the scheme and domain. It saves some redundancy from
|
|
||||||
* database and the urls could still work after a domain change.
|
|
||||||
*
|
|
||||||
* @param url the full url to the episode.
|
|
||||||
*/
|
|
||||||
fun SEpisode.setUrlWithoutDomain(url: String) {
|
|
||||||
this.url = getUrlWithoutDomain(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns the url of the anime without the scheme and domain. It saves some redundancy from
|
|
||||||
* database and the urls could still work after a domain change.
|
|
||||||
*
|
|
||||||
* @param url the full url to the anime.
|
|
||||||
*/
|
|
||||||
fun SAnime.setUrlWithoutDomain(url: String) {
|
|
||||||
this.url = getUrlWithoutDomain(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the url of the given string without the scheme and domain.
|
|
||||||
*
|
|
||||||
* @param orig the full url.
|
|
||||||
*/
|
|
||||||
private fun getUrlWithoutDomain(orig: String): String {
|
|
||||||
return try {
|
|
||||||
val uri = URI(orig)
|
|
||||||
var out = uri.path
|
|
||||||
if (uri.query != null) {
|
|
||||||
out += "?" + uri.query
|
|
||||||
}
|
|
||||||
if (uri.fragment != null) {
|
|
||||||
out += "#" + uri.fragment
|
|
||||||
}
|
|
||||||
out
|
|
||||||
} catch (e: URISyntaxException) {
|
|
||||||
orig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before inserting a new episode into database. Use it if you need to override episode
|
|
||||||
* fields, like the title or the episode number. Do not change anything to [anime].
|
|
||||||
*
|
|
||||||
* @param episode the episode to be added.
|
|
||||||
* @param anime the anime of the episode.
|
|
||||||
*/
|
|
||||||
open fun prepareNewEpisode(episode: SEpisode, anime: SAnime) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of filters for the source.
|
|
||||||
*/
|
|
||||||
override fun getFilterList() = AnimeFilterList()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source.online
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
fun AnimeHttpSource.getImageUrl(page: Page): Observable<Page> {
|
|
||||||
page.status = Page.LOAD_PAGE
|
|
||||||
return fetchImageUrl(page)
|
|
||||||
.doOnError { page.status = Page.ERROR }
|
|
||||||
.onErrorReturn { null }
|
|
||||||
.doOnNext { page.imageUrl = it }
|
|
||||||
.map { page }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
|
||||||
return Observable.from(pages)
|
|
||||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
|
||||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
|
||||||
return Observable.from(pages)
|
|
||||||
.filter { it.imageUrl.isNullOrEmpty() }
|
|
||||||
.concatMap { getImageUrl(it) }
|
|
||||||
}
|
|
||||||
-222
@@ -1,222 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animesource.online
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
|
||||||
*/
|
|
||||||
abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [AnimesPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val animes = document.select(popularAnimeSelector()).map { element ->
|
|
||||||
popularAnimeFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = popularAnimeNextPageSelector()?.let { selector ->
|
|
||||||
document.select(selector).first()
|
|
||||||
} != null
|
|
||||||
|
|
||||||
return AnimesPage(animes, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
|
|
||||||
*/
|
|
||||||
protected abstract fun popularAnimeSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
|
|
||||||
* totally fine to fill only those two values.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [popularAnimeSelector].
|
|
||||||
*/
|
|
||||||
protected abstract fun popularAnimeFromElement(element: Element): SAnime
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
|
||||||
* there's no next page.
|
|
||||||
*/
|
|
||||||
protected abstract fun popularAnimeNextPageSelector(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [AnimesPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val animes = document.select(searchAnimeSelector()).map { element ->
|
|
||||||
searchAnimeFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
|
|
||||||
document.select(selector).first()
|
|
||||||
} != null
|
|
||||||
|
|
||||||
return AnimesPage(animes, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
|
|
||||||
*/
|
|
||||||
protected abstract fun searchAnimeSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
|
|
||||||
* totally fine to fill only those two values.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [searchAnimeSelector].
|
|
||||||
*/
|
|
||||||
protected abstract fun searchAnimeFromElement(element: Element): SAnime
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
|
||||||
* there's no next page.
|
|
||||||
*/
|
|
||||||
protected abstract fun searchAnimeNextPageSelector(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [AnimesPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val animes = document.select(latestUpdatesSelector()).map { element ->
|
|
||||||
latestUpdatesFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
|
||||||
document.select(selector).first()
|
|
||||||
} != null
|
|
||||||
|
|
||||||
return AnimesPage(animes, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
|
|
||||||
*/
|
|
||||||
protected abstract fun latestUpdatesSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
|
|
||||||
* totally fine to fill only those two values.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [latestUpdatesSelector].
|
|
||||||
*/
|
|
||||||
protected abstract fun latestUpdatesFromElement(element: Element): SAnime
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
|
||||||
* there's no next page.
|
|
||||||
*/
|
|
||||||
protected abstract fun latestUpdatesNextPageSelector(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the details of a anime.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun animeDetailsParse(response: Response): SAnime {
|
|
||||||
return animeDetailsParse(response.asJsoup())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the details of the anime from the given [document].
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
*/
|
|
||||||
protected abstract fun animeDetailsParse(document: Document): SAnime
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of episodes.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
return document.select(episodeListSelector()).map { episodeFromElement(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
|
|
||||||
*/
|
|
||||||
protected abstract fun episodeListSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of episodes.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun episodeLinkParse(response: Response): String {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
return linkFromElement(document.select(episodeLinkSelector()).first())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
|
|
||||||
*/
|
|
||||||
protected abstract fun episodeLinkSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a episode from the given element.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [episodeListSelector].
|
|
||||||
*/
|
|
||||||
protected abstract fun episodeFromElement(element: Element): SEpisode
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a episode from the given element.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [episodeListSelector].
|
|
||||||
*/
|
|
||||||
protected abstract fun linkFromElement(element: Element): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the page list.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
return pageListParse(response.asJsoup())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a page list from the given document.
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
*/
|
|
||||||
protected abstract fun pageListParse(document: Document): List<Page>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and returns the absolute url to the source image.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
return imageUrlParse(response.asJsoup())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute url to the source image from the document.
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
*/
|
|
||||||
protected abstract fun imageUrlParse(document: Document): String
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.annoations
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Nsfw
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.os.Build
|
|
||||||
// import android.os.Handler
|
|
||||||
// import android.os.Looper
|
|
||||||
// import android.webkit.WebSettings
|
|
||||||
// import android.webkit.WebView
|
|
||||||
// import android.widget.Toast
|
|
||||||
// import eu.kanade.tachiyomi.R
|
|
||||||
// import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
|
||||||
// import eu.kanade.tachiyomi.util.system.WebViewUtil
|
|
||||||
// import eu.kanade.tachiyomi.util.system.isOutdated
|
|
||||||
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
|
||||||
// import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Response
|
|
||||||
// import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class CloudflareInterceptor() : Interceptor {
|
|
||||||
|
|
||||||
// private val handler = Handler(Looper.getMainLooper())
|
|
||||||
|
|
||||||
// private val networkHelper = NetworkHelper()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
|
||||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
|
||||||
* Application class.
|
|
||||||
*/
|
|
||||||
// private val initWebView by lazy {
|
|
||||||
// WebSettings.getDefaultUserAgent(context)
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
return chain.proceed(originalRequest)
|
|
||||||
|
|
||||||
// if (!WebViewUtil.supportsWebView(context)) {
|
|
||||||
// launchUI {
|
|
||||||
// context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
|
||||||
// }
|
|
||||||
// return chain.proceed(originalRequest)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// initWebView
|
|
||||||
//
|
|
||||||
// val response = chain.proceed(originalRequest)
|
|
||||||
//
|
|
||||||
// // Check if Cloudflare anti-bot is on
|
|
||||||
// if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
|
||||||
// return response
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// response.close()
|
|
||||||
// networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
|
|
||||||
// val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
|
|
||||||
// .firstOrNull { it.name == "cf_clearance" }
|
|
||||||
// resolveWithWebView(originalRequest, oldCookie)
|
|
||||||
//
|
|
||||||
// return chain.proceed(originalRequest)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
|
||||||
// // we don't crash the entire app
|
|
||||||
// throw IOException(e)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// // @SuppressLint("SetJavaScriptEnabled")
|
|
||||||
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
|
||||||
// // We need to lock this thread until the WebView finds the challenge solution url, because
|
|
||||||
// // OkHttp doesn't support asynchronous interceptors.
|
|
||||||
// val latch = CountDownLatch(1)
|
|
||||||
//
|
|
||||||
// var webView: WebView? = null
|
|
||||||
//
|
|
||||||
// var challengeFound = false
|
|
||||||
// var cloudflareBypassed = false
|
|
||||||
// var isWebViewOutdated = false
|
|
||||||
//
|
|
||||||
// val origRequestUrl = request.url.toString()
|
|
||||||
// val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
|
||||||
// headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
|
||||||
//
|
|
||||||
// handler.post {
|
|
||||||
// val webview = WebView(context)
|
|
||||||
// webView = webview
|
|
||||||
// webview.setDefaultSettings()
|
|
||||||
//
|
|
||||||
// // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
|
||||||
// webview.settings.userAgentString = request.header("User-Agent")
|
|
||||||
// ?: HttpSource.DEFAULT_USERAGENT
|
|
||||||
//
|
|
||||||
// webview.webViewClient = object : WebViewClientCompat() {
|
|
||||||
// override fun onPageFinished(view: WebView, url: String) {
|
|
||||||
// fun isCloudFlareBypassed(): Boolean {
|
|
||||||
// return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
|
|
||||||
// .firstOrNull { it.name == "cf_clearance" }
|
|
||||||
// .let { it != null && it != oldCookie }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (isCloudFlareBypassed()) {
|
|
||||||
// cloudflareBypassed = true
|
|
||||||
// latch.countDown()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // HTTP error codes are only received since M
|
|
||||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
|
||||||
// url == origRequestUrl && !challengeFound
|
|
||||||
// ) {
|
|
||||||
// // The first request didn't return the challenge, abort.
|
|
||||||
// latch.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onReceivedErrorCompat(
|
|
||||||
// view: WebView,
|
|
||||||
// errorCode: Int,
|
|
||||||
// description: String?,
|
|
||||||
// failingUrl: String,
|
|
||||||
// isMainFrame: Boolean
|
|
||||||
// ) {
|
|
||||||
// if (isMainFrame) {
|
|
||||||
// if (errorCode == 503) {
|
|
||||||
// // Found the Cloudflare challenge page.
|
|
||||||
// challengeFound = true
|
|
||||||
// } else {
|
|
||||||
// // Unlock thread, the challenge wasn't found.
|
|
||||||
// latch.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// webView?.loadUrl(origRequestUrl, headers)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Wait a reasonable amount of time to retrieve the solution. The minimum should be
|
|
||||||
// // around 4 seconds but it can take more due to slow networks or server issues.
|
|
||||||
// latch.await(12, TimeUnit.SECONDS)
|
|
||||||
//
|
|
||||||
// handler.post {
|
|
||||||
// if (!cloudflareBypassed) {
|
|
||||||
// isWebViewOutdated = webView?.isOutdated() == true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// webView?.stopLoading()
|
|
||||||
// webView?.destroy()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Throw exception if we failed to bypass Cloudflare
|
|
||||||
// if (!cloudflareBypassed) {
|
|
||||||
// // Prompt user to update WebView if it seems too outdated
|
|
||||||
// if (isWebViewOutdated) {
|
|
||||||
// context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// companion object {
|
|
||||||
// private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
|
||||||
// private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -10,69 +10,63 @@ package eu.kanade.tachiyomi.network
|
|||||||
// import android.content.Context
|
// import android.content.Context
|
||||||
// import eu.kanade.tachiyomi.BuildConfig
|
// import eu.kanade.tachiyomi.BuildConfig
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import android.content.Context
|
|
||||||
// import okhttp3.HttpUrl.Companion.toHttpUrl
|
// import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
// import okhttp3.dnsoverhttps.DnsOverHttps
|
// import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
// import okhttp3.logging.HttpLoggingInterceptor
|
// import okhttp3.logging.HttpLoggingInterceptor
|
||||||
// import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
// private val preferences: PreferencesHelper by injectLazy()
|
// private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
// private val cacheDir = File(context.cacheDir, "network_cache")
|
// private val cacheDir = File(context.cacheDir, "network_cache")
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
// private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
val cookieManager = MemoryCookieJar()
|
val cookieManager = PersistentCookieJar(context)
|
||||||
|
|
||||||
val client by lazy {
|
private val baseClientBuilder: OkHttpClient.Builder
|
||||||
val builder = OkHttpClient.Builder()
|
get() {
|
||||||
.cookieJar(cookieManager)
|
val builder = OkHttpClient.Builder()
|
||||||
// .cache(Cache(cacheDir, cacheSize))
|
.cookieJar(cookieManager)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(5, TimeUnit.MINUTES)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.writeTimeout(5, TimeUnit.MINUTES)
|
.addInterceptor(UserAgentInterceptor())
|
||||||
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
|
||||||
|
|
||||||
// .addInterceptor(UserAgentInterceptor())
|
if (serverConfig.debugLogsEnabled) {
|
||||||
|
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BASIC
|
||||||
|
}
|
||||||
|
builder.addInterceptor(httpLoggingInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
// if (BuildConfig.DEBUG) {
|
// when (preferences.dohProvider()) {
|
||||||
// val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
// PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||||
// level = HttpLoggingInterceptor.Level.HEADERS
|
// PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||||
// }
|
// }
|
||||||
// builder.addInterceptor(httpLoggingInterceptor)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (preferences.enableDoh()) {
|
return builder
|
||||||
// builder.dns(
|
}
|
||||||
// DnsOverHttps.Builder().client(builder.build())
|
|
||||||
// .url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
|
||||||
// .bootstrapDnsHosts(
|
|
||||||
// listOf(
|
|
||||||
// InetAddress.getByName("162.159.36.1"),
|
|
||||||
// InetAddress.getByName("162.159.46.1"),
|
|
||||||
// InetAddress.getByName("1.1.1.1"),
|
|
||||||
// InetAddress.getByName("1.0.0.1"),
|
|
||||||
// InetAddress.getByName("162.159.132.53"),
|
|
||||||
// InetAddress.getByName("2606:4700:4700::1111"),
|
|
||||||
// InetAddress.getByName("2606:4700:4700::1001"),
|
|
||||||
// InetAddress.getByName("2606:4700:4700::0064"),
|
|
||||||
// InetAddress.getByName("2606:4700:4700::6400")
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// .build()
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
builder.build()
|
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||||
}
|
val client by lazy { baseClientBuilder.build() }
|
||||||
|
|
||||||
val cloudflareClient by lazy {
|
val cloudflareClient by lazy {
|
||||||
client.newBuilder()
|
client.newBuilder()
|
||||||
.addInterceptor(CloudflareInterceptor())
|
.addInterceptor(CloudflareInterceptor())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tachidesk -->
|
||||||
|
val cookies: PersistentCookieStore
|
||||||
|
get() = cookieManager.store
|
||||||
|
// Tachidesk <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
// import kotlinx.coroutines.suspendCancellableCoroutine
|
// import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
@@ -48,36 +53,38 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
// suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
suspend fun Call.await(): Response {
|
||||||
// return suspendCancellableCoroutine { continuation ->
|
return suspendCancellableCoroutine { continuation ->
|
||||||
// enqueue(
|
enqueue(
|
||||||
// object : Callback {
|
object : Callback {
|
||||||
// override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
// if (assertSuccess && !response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
// continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// continuation.resume(response)
|
continuation.resume(response) {
|
||||||
// }
|
response.body?.closeQuietly()
|
||||||
//
|
}
|
||||||
// override fun onFailure(call: Call, e: IOException) {
|
}
|
||||||
// // Don't bother with resuming the continuation if it is already cancelled.
|
|
||||||
// if (continuation.isCancelled) return
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
// continuation.resumeWithException(e)
|
// Don't bother with resuming the continuation if it is already cancelled.
|
||||||
// }
|
if (continuation.isCancelled) return
|
||||||
// }
|
continuation.resumeWithException(e)
|
||||||
// )
|
}
|
||||||
//
|
}
|
||||||
// continuation.invokeOnCancellation {
|
)
|
||||||
// try {
|
|
||||||
// cancel()
|
continuation.invokeOnCancellation {
|
||||||
// } catch (ex: Throwable) {
|
try {
|
||||||
// // Ignore cancel exception
|
cancel()
|
||||||
// }
|
} catch (ex: Throwable) {
|
||||||
// }
|
// Ignore cancel exception
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
return asObservable()
|
return asObservable()
|
||||||
@@ -103,6 +110,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
|||||||
// return progressClient.newCall(request)
|
// return progressClient.newCall(request)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
// .cache(null)
|
// .cache(null)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
// from TachiWeb-Server
|
||||||
|
class PersistentCookieJar(context: Context) : CookieJar {
|
||||||
|
|
||||||
|
val store = PersistentCookieStore(context)
|
||||||
|
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
store.addAll(url, cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
return store.get(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
// from TachiWeb-Server
|
||||||
|
class PersistentCookieStore(context: Context) {
|
||||||
|
|
||||||
|
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||||
|
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
init {
|
||||||
|
for ((key, value) in prefs.all) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val cookies = value as? Set<String>
|
||||||
|
if (cookies != null) {
|
||||||
|
try {
|
||||||
|
val url = "http://$key".toHttpUrlOrNull() ?: continue
|
||||||
|
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||||
|
.filter { !it.hasExpired() }
|
||||||
|
cookieMap.put(key, nonExpiredCookies)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
val key = url.toUri().host
|
||||||
|
|
||||||
|
// Append or replace the cookies for this domain.
|
||||||
|
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||||
|
for (cookie in cookies) {
|
||||||
|
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||||
|
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
|
||||||
|
if (pos == -1) {
|
||||||
|
cookiesForDomain.add(cookie)
|
||||||
|
} else {
|
||||||
|
cookiesForDomain[pos] = cookie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cookieMap.put(key, cookiesForDomain)
|
||||||
|
|
||||||
|
// Get cookies to be stored in disk
|
||||||
|
val newValues = cookiesForDomain.asSequence()
|
||||||
|
.filter { it.persistent && !it.hasExpired() }
|
||||||
|
.map(Cookie::toString)
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
prefs.edit().putStringSet(key, newValues).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeAll() {
|
||||||
|
prefs.edit().clear().apply()
|
||||||
|
cookieMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(uri: URI) {
|
||||||
|
prefs.edit().remove(uri.host).apply()
|
||||||
|
cookieMap.remove(uri.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(url: HttpUrl) = get(url.toUri().host)
|
||||||
|
|
||||||
|
fun get(uri: URI) = get(uri.host)
|
||||||
|
|
||||||
|
private fun get(url: String): List<Cookie> {
|
||||||
|
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.BrowserVersion
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
// from TachiWeb-Server
|
||||||
|
class CloudflareInterceptor : Interceptor {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
logger.trace { "CloudflareInterceptor is being used." }
|
||||||
|
|
||||||
|
val response = chain.proceed(originalRequest)
|
||||||
|
|
||||||
|
// Check if Cloudflare anti-bot is on
|
||||||
|
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
|
||||||
|
|
||||||
|
return try {
|
||||||
|
response.close()
|
||||||
|
network.cookies.remove(originalRequest.url.toUri())
|
||||||
|
|
||||||
|
chain.proceed(resolveChallenge(response))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
|
// we don't crash the entire app
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveChallenge(response: Response): Request {
|
||||||
|
val browserVersion = BrowserVersion.BrowserVersionBuilder(BrowserVersion.BEST_SUPPORTED)
|
||||||
|
.setUserAgent(response.request.header("User-Agent") ?: BrowserVersion.BEST_SUPPORTED.userAgent)
|
||||||
|
.build()
|
||||||
|
val convertedCookies = WebClient(browserVersion).use { webClient ->
|
||||||
|
webClient.options.isThrowExceptionOnFailingStatusCode = false
|
||||||
|
webClient.options.isThrowExceptionOnScriptError = false
|
||||||
|
webClient.getPage<HtmlPage>(response.request.url.toString())
|
||||||
|
webClient.waitForBackgroundJavaScript(10000)
|
||||||
|
// Challenge solved, process cookies
|
||||||
|
webClient.cookieManager.cookies.filter {
|
||||||
|
// Only include Cloudflare cookies
|
||||||
|
it.name.startsWith("__cf") || it.name.startsWith("cf_")
|
||||||
|
}.map {
|
||||||
|
// Convert cookies -> OkHttp format
|
||||||
|
Cookie.Builder()
|
||||||
|
.domain(it.domain.removePrefix("."))
|
||||||
|
.expiresAt(it.expires?.time ?: Long.MAX_VALUE)
|
||||||
|
.name(it.name)
|
||||||
|
.path(it.path)
|
||||||
|
.value(it.value).apply {
|
||||||
|
if (it.isHttpOnly) httpOnly()
|
||||||
|
if (it.isSecure) secure()
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy cookies to cookie store
|
||||||
|
convertedCookies.forEach {
|
||||||
|
network.cookies.addAll(
|
||||||
|
HttpUrl.Builder()
|
||||||
|
.scheme("http")
|
||||||
|
.host(it.domain)
|
||||||
|
.build(),
|
||||||
|
listOf(it)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Merge new and existing cookies for this request
|
||||||
|
// Find the cookies that we need to merge into this request
|
||||||
|
val convertedForThisRequest = convertedCookies.filter {
|
||||||
|
it.matches(response.request.url)
|
||||||
|
}
|
||||||
|
// Extract cookies from current request
|
||||||
|
val existingCookies = Cookie.parseAll(
|
||||||
|
response.request.url,
|
||||||
|
response.request.headers
|
||||||
|
)
|
||||||
|
// Filter out existing values of cookies that we are about to merge in
|
||||||
|
val filteredExisting = existingCookies.filter { existing ->
|
||||||
|
convertedForThisRequest.none { converted -> converted.name == existing.name }
|
||||||
|
}
|
||||||
|
val newCookies = filteredExisting + convertedForThisRequest
|
||||||
|
return response.request.newBuilder()
|
||||||
|
.header("Cookie", newCookies.map { it.toString() }.joinToString("; "))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
|
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class UserAgentInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
|
||||||
|
val newRequest = originalRequest
|
||||||
|
.newBuilder()
|
||||||
|
.removeHeader("User-Agent")
|
||||||
|
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
||||||
|
.build()
|
||||||
|
chain.proceed(newRequest)
|
||||||
|
} else {
|
||||||
|
chain.proceed(originalRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
// import com.github.junrar.Archive
|
|
||||||
// import com.google.gson.JsonParser
|
|
||||||
// import eu.kanade.tachiyomi.R
|
|
||||||
// import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
// import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
// import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
// import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
// import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
// import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
// import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
|
||||||
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|
||||||
// import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
// import eu.kanade.tachiyomi.util.storage.EpubFile
|
|
||||||
// import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
// import rx.Observable
|
|
||||||
// import timber.log.Timber
|
|
||||||
// import java.io.File
|
|
||||||
// import java.io.FileInputStream
|
|
||||||
// import java.io.InputStream
|
|
||||||
// import java.util.Locale
|
|
||||||
// import java.util.concurrent.TimeUnit
|
|
||||||
// import java.util.zip.ZipFile
|
|
||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
|
||||||
companion object {
|
|
||||||
const val ID = 0L
|
|
||||||
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
|
||||||
//
|
|
||||||
// private const val COVER_NAME = "cover.jpg"
|
|
||||||
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
|
||||||
//
|
|
||||||
// private val POPULAR_FILTERS = FilterList(OrderBy())
|
|
||||||
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
|
||||||
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
|
||||||
//
|
|
||||||
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
|
||||||
// val dir = getBaseDirectories(context).firstOrNull()
|
|
||||||
// if (dir == null) {
|
|
||||||
// input.close()
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
|
||||||
//
|
|
||||||
// // It might not exist if using the external SD card
|
|
||||||
// cover.parentFile?.mkdirs()
|
|
||||||
// input.use {
|
|
||||||
// cover.outputStream().use {
|
|
||||||
// input.copyTo(it)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return cover
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun getBaseDirectories(context: Context): List<File> {
|
|
||||||
// val c = context.getString(R.string.app_name) + File.separator + "local"
|
|
||||||
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
override val id = ID
|
|
||||||
override val name = "Local source"
|
|
||||||
override val lang = ""
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// override fun toString() = context.getString(R.string.local_source)
|
|
||||||
//
|
|
||||||
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
|
||||||
//
|
|
||||||
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
// val baseDirs = getBaseDirectories(context)
|
|
||||||
//
|
|
||||||
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
|
||||||
// var mangaDirs = baseDirs
|
|
||||||
// .asSequence()
|
|
||||||
// .mapNotNull { it.listFiles()?.toList() }
|
|
||||||
// .flatten()
|
|
||||||
// .filter { it.isDirectory }
|
|
||||||
// .filterNot { it.name.startsWith('.') }
|
|
||||||
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
|
||||||
// .distinctBy { it.name }
|
|
||||||
//
|
|
||||||
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
|
||||||
// when (state?.index) {
|
|
||||||
// 0 -> {
|
|
||||||
// mangaDirs = if (state.ascending) {
|
|
||||||
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
|
||||||
// } else {
|
|
||||||
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// 1 -> {
|
|
||||||
// mangaDirs = if (state.ascending) {
|
|
||||||
// mangaDirs.sortedBy(File::lastModified)
|
|
||||||
// } else {
|
|
||||||
// mangaDirs.sortedByDescending(File::lastModified)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val mangas = mangaDirs.map { mangaDir ->
|
|
||||||
// SManga.create().apply {
|
|
||||||
// title = mangaDir.name
|
|
||||||
// url = mangaDir.name
|
|
||||||
//
|
|
||||||
// // Try to find the cover
|
|
||||||
// for (dir in baseDirs) {
|
|
||||||
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
|
||||||
// if (cover.exists()) {
|
|
||||||
// thumbnail_url = cover.absolutePath
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val chapters = fetchChapterList(this).toBlocking().first()
|
|
||||||
// if (chapters.isNotEmpty()) {
|
|
||||||
// val chapter = chapters.last()
|
|
||||||
// val format = getFormat(chapter)
|
|
||||||
// if (format is Format.Epub) {
|
|
||||||
// EpubFile(format.file).use { epub ->
|
|
||||||
// epub.fillMangaMetadata(this)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Copy the cover from the first chapter found.
|
|
||||||
// if (thumbnail_url == null) {
|
|
||||||
// try {
|
|
||||||
// val dest = updateCover(chapter, this)
|
|
||||||
// thumbnail_url = dest?.absolutePath
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// Timber.e(e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return Observable.just(MangasPage(mangas.toList(), false))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
|
||||||
//
|
|
||||||
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
// getBaseDirectories(context)
|
|
||||||
// .asSequence()
|
|
||||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
|
||||||
// .flatten()
|
|
||||||
// .firstOrNull { it.extension == "json" }
|
|
||||||
// ?.apply {
|
|
||||||
// val reader = this.inputStream().bufferedReader()
|
|
||||||
// val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
//
|
|
||||||
// manga.title = json["title"]?.asString ?: manga.title
|
|
||||||
// manga.author = json["author"]?.asString ?: manga.author
|
|
||||||
// manga.artist = json["artist"]?.asString ?: manga.artist
|
|
||||||
// manga.description = json["description"]?.asString ?: manga.description
|
|
||||||
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
|
|
||||||
// ?: manga.genre
|
|
||||||
// manga.status = json["status"]?.asInt ?: manga.status
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return Observable.just(manga)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
// val chapters = getBaseDirectories(context)
|
|
||||||
// .asSequence()
|
|
||||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
|
||||||
// .flatten()
|
|
||||||
// .filter { it.isDirectory || isSupportedFile(it.extension) }
|
|
||||||
// .map { chapterFile ->
|
|
||||||
// SChapter.create().apply {
|
|
||||||
// url = "${manga.url}/${chapterFile.name}"
|
|
||||||
// name = if (chapterFile.isDirectory) {
|
|
||||||
// chapterFile.name
|
|
||||||
// } else {
|
|
||||||
// chapterFile.nameWithoutExtension
|
|
||||||
// }
|
|
||||||
// date_upload = chapterFile.lastModified()
|
|
||||||
//
|
|
||||||
// val format = getFormat(this)
|
|
||||||
// if (format is Format.Epub) {
|
|
||||||
// EpubFile(format.file).use { epub ->
|
|
||||||
// epub.fillChapterMetadata(this)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val chapNameCut = stripMangaTitle(name, manga.title)
|
|
||||||
// if (chapNameCut.isNotEmpty()) name = chapNameCut
|
|
||||||
// ChapterRecognition.parseChapterNumber(this, manga)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .sortedWith(
|
|
||||||
// Comparator { c1, c2 ->
|
|
||||||
// val c = c2.chapter_number.compareTo(c1.chapter_number)
|
|
||||||
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// .toList()
|
|
||||||
//
|
|
||||||
// return Observable.just(chapters)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
|
|
||||||
// * characters.
|
|
||||||
// */
|
|
||||||
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
|
|
||||||
// var chapterNameIndex = 0
|
|
||||||
// var mangaTitleIndex = 0
|
|
||||||
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
|
||||||
// val chapterChar = chapterName[chapterNameIndex]
|
|
||||||
// val mangaChar = mangaTitle[mangaTitleIndex]
|
|
||||||
// if (!chapterChar.equals(mangaChar, true)) {
|
|
||||||
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
|
||||||
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
|
||||||
//
|
|
||||||
// if (!invalidChapterChar && !invalidMangaChar) {
|
|
||||||
// return chapterName
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (invalidChapterChar) {
|
|
||||||
// chapterNameIndex++
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (invalidMangaChar) {
|
|
||||||
// mangaTitleIndex++
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// chapterNameIndex++
|
|
||||||
// mangaTitleIndex++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
// return Observable.error(Exception("Unused"))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun isSupportedFile(extension: String): Boolean {
|
|
||||||
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun getFormat(chapter: SChapter): Format {
|
|
||||||
// val baseDirs = getBaseDirectories(context)
|
|
||||||
//
|
|
||||||
// for (dir in baseDirs) {
|
|
||||||
// val chapFile = File(dir, chapter.url)
|
|
||||||
// if (!chapFile.exists()) continue
|
|
||||||
//
|
|
||||||
// return getFormat(chapFile)
|
|
||||||
// }
|
|
||||||
// throw Exception("Chapter not found")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun getFormat(file: File): Format {
|
|
||||||
// val extension = file.extension
|
|
||||||
// return if (file.isDirectory) {
|
|
||||||
// Format.Directory(file)
|
|
||||||
// } else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
|
||||||
// Format.Zip(file)
|
|
||||||
// } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
|
||||||
// Format.Rar(file)
|
|
||||||
// } else if (extension.equals("epub", true)) {
|
|
||||||
// Format.Epub(file)
|
|
||||||
// } else {
|
|
||||||
// throw Exception("Invalid chapter format")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
|
||||||
// return when (val format = getFormat(chapter)) {
|
|
||||||
// is Format.Directory -> {
|
|
||||||
// val entry = format.file.listFiles()
|
|
||||||
// ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
|
||||||
// ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, it.inputStream()) }
|
|
||||||
// }
|
|
||||||
// is Format.Zip -> {
|
|
||||||
// ZipFile(format.file).use { zip ->
|
|
||||||
// val entry = zip.entries().toList()
|
|
||||||
// .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
|
||||||
// .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// is Format.Rar -> {
|
|
||||||
// Archive(format.file).use { archive ->
|
|
||||||
// val entry = archive.fileHeaders
|
|
||||||
// .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
|
||||||
// .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// is Format.Epub -> {
|
|
||||||
// EpubFile(format.file).use { epub ->
|
|
||||||
// val entry = epub.getImagesFromPages()
|
|
||||||
// .firstOrNull()
|
|
||||||
// ?.let { epub.getEntry(it) }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
|
|
||||||
//
|
|
||||||
// override fun getFilterList() = FilterList(OrderBy())
|
|
||||||
//
|
|
||||||
// sealed class Format {
|
|
||||||
// data class Directory(val file: File) : Format()
|
|
||||||
// data class Zip(val file: File) : Format()
|
|
||||||
// data class Rar(val file: File) : Format()
|
|
||||||
// data class Epub(val file: File) : Format()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
|
||||||
|
|
||||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
|
||||||
|
|
||||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
createInternalSources().forEach { registerSource(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun get(sourceKey: Long): Source? {
|
|
||||||
return sourcesMap[sourceKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOrStub(sourceKey: Long): Source {
|
|
||||||
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
|
||||||
StubSource(sourceKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
|
||||||
|
|
||||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
|
||||||
|
|
||||||
internal fun registerSource(source: Source, overwrite: Boolean = false) {
|
|
||||||
if (overwrite || !sourcesMap.containsKey(source.id)) {
|
|
||||||
sourcesMap[source.id] = source
|
|
||||||
}
|
|
||||||
if (overwrite || !stubSourcesMap.containsKey(source.id)) {
|
|
||||||
stubSourcesMap[source.id] = StubSource(source.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun unregisterSource(source: Source) {
|
|
||||||
sourcesMap.remove(source.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createInternalSources(): List<Source> = listOf(
|
|
||||||
// LocalSource(context)
|
|
||||||
)
|
|
||||||
|
|
||||||
private inner class StubSource(override val id: Long) : Source {
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = id.toString()
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return Observable.error(getSourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
return Observable.error(getSourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
return Observable.error(getSourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSourceNotInstalledException(): Exception {
|
|
||||||
// return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
|
||||||
return Exception("source not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user