Compare commits
361 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ddb5db57b | |||
| 4f70cc9283 | |||
| 23b643d637 | |||
| fdfc256c4d | |||
| fba56c1b75 | |||
| 4743bfacf7 | |||
| 2356537f7c | |||
| fa071aee84 | |||
| c00ca23a8b | |||
| 733b017936 | |||
| 4147f2e368 | |||
| 154b9992eb | |||
| 88b881b043 | |||
| 5d1491fb8c | |||
| 3a33196cf1 | |||
| fa8e0478da | |||
| 7e7e069244 | |||
| 18e0d34af0 | |||
| 3fe3f35483 | |||
| cf8e274883 | |||
| 10dee8b345 | |||
| ae8d30593f | |||
| 9cde46b5da | |||
| 8e61632155 | |||
| e2c4b4cb57 | |||
| 326da504ea | |||
| c5874a3f10 | |||
| 02802fab97 | |||
| 29dea10be2 | |||
| 6bc36193dc | |||
| 81e123388e | |||
| 8ebd7869a5 | |||
| 7a2f5f13f1 | |||
| 25d7dad39f | |||
| c8f8795920 | |||
| 84206a7074 | |||
| 6fd8b36dca | |||
| d1500baae1 | |||
| 045801dd1a | |||
| 14a2cbc793 | |||
| fd385017df | |||
| 9b05954cf2 | |||
| 6aaf636069 | |||
| d30e89e5ec | |||
| 7acc745478 | |||
| 5a9a2d816e | |||
| 105f11ed02 | |||
| 3021437a05 | |||
| 439602fc03 | |||
| 34e13b9589 | |||
| 2aab4ae918 | |||
| 7ef67671a4 | |||
| e8df84416c | |||
| be930bb68b | |||
| db52948865 | |||
| d2a72526f6 | |||
| 0a9f57b32b | |||
| 180f210536 | |||
| c1baa31eed | |||
| cacc97cec7 | |||
| d5691fd81c | |||
| 49dc9fe5f6 | |||
| c0b49c7428 | |||
| fa345af42d | |||
| db3cc786a1 | |||
| fe879ae51d | |||
| 2f55460ffb | |||
| fbc5bd4642 | |||
| 5e0c7d3c9d | |||
| 083996a48d | |||
| 9d38f478e3 | |||
| 57274a0a01 | |||
| b3b56b7fc8 | |||
| 0b690577da | |||
| e9683a3a37 | |||
| f8f67b3eba | |||
| 7b16b082d8 | |||
| 2a783f0d8e | |||
| 42ae32de33 | |||
| cec7ddc486 | |||
| 9c55fc3868 | |||
| 104c5a8d83 | |||
| 7450b16742 | |||
| 3ecd0931a1 | |||
| 2f2a52ae2f | |||
| f464087c30 | |||
| 2364960388 | |||
| 76be4d64cd | |||
| 7d98e8ce47 | |||
| 40831fc681 | |||
| e38e7ccf26 | |||
| 98b9e2f2cf | |||
| 4bf3c12f76 | |||
| bab25f9ad9 | |||
| a62ee8f8c3 | |||
| 5f23691e20 | |||
| 3de9ccc62f | |||
| 1896f7f37b | |||
| 490643dc02 | |||
| 9808976088 | |||
| 5a73068a10 | |||
| 01d5c2540d | |||
| 866b01f865 | |||
| da6a953099 | |||
| bce8d58845 | |||
| 3cfce2db04 | |||
| 327aae5dd9 | |||
| 1bdfde7032 | |||
| 295a0817b0 | |||
| a02dc02d52 | |||
| dc012edf7d | |||
| 1e2eb11c13 | |||
| 3a825f4f25 | |||
| b9ea8c5f74 | |||
| 320d7e2536 | |||
| c200785479 | |||
| 8abb132ad6 | |||
| 8bb2269f36 | |||
| 9d17b26283 | |||
| 5909f15db7 | |||
| 11672ca576 | |||
| e09773def3 | |||
| f6d4432e6f | |||
| 45a6abc5c2 | |||
| dc5e677a38 | |||
| a82549dc17 | |||
| a002e19d9d | |||
| cdf1f98d28 | |||
| 0ff1ebdeb7 | |||
| 17f4a396f8 | |||
| 8aa3cf4368 | |||
| 0136c5e493 | |||
| 8b94b9ee80 | |||
| bed63f19f2 | |||
| e2a6545a84 | |||
| e3d3ec6895 | |||
| 7ba476bd79 | |||
| 2dd41ebd27 | |||
| 038df78171 | |||
| 6e5ff2b508 | |||
| ec8d1e8680 | |||
| 1f0f0c33b7 | |||
| 825940fcac | |||
| 4618834af2 | |||
| 55d968df5e | |||
| 63a078cf7d | |||
| 5304917e53 | |||
| 831b74d2ec | |||
| 1bad9dcd69 | |||
| dd43716851 | |||
| f2e55e95a2 | |||
| 14658a0c4d | |||
| 4195e7056b | |||
| 1d29e8b248 | |||
| b718c718df | |||
| a3601cf1b5 | |||
| 0236a9639b | |||
| 5f4c7454ee | |||
| 773120c96a | |||
| 4b273c6bf9 | |||
| b626aa66ba | |||
| 1dd029559e | |||
| 59cbe5d5bc | |||
| 40d1173653 | |||
| bf6a0aba5d | |||
| 34d8feacdd | |||
| 1ea821584c | |||
| 3d2fee19bb | |||
| 449d12779a | |||
| 6fb6a251e7 | |||
| 4d6220f894 | |||
| fe747bfc52 | |||
| 0c2d038870 | |||
| 4e3f73af75 | |||
| 63e5e1b45f | |||
| 2e1558bd96 | |||
| 0671dee8b2 | |||
| 8f91b8089a | |||
| 009b45f676 | |||
| 8f7d5eb311 | |||
| f3de835ef3 | |||
| fd6662f428 | |||
| fde137b3ed | |||
| a1349aa0e3 | |||
| c9ef5f9b9d | |||
| 8fbf564177 | |||
| ae0b1a818c | |||
| c04cc780b7 | |||
| 71ad1bb6e3 | |||
| c1be77ee9b | |||
| d1fa857ffb | |||
| 93fd81b38b | |||
| 2f116b40b2 | |||
| b884d34bdf | |||
| 309803368b | |||
| 19fc5be8f3 | |||
| c28fac14c0 | |||
| 66e38de29f | |||
| 282cb1d3be | |||
| b741ded595 | |||
| 6b290695fc | |||
| 4e43c554c0 | |||
| 090a72b35f | |||
| 3fcc269df3 | |||
| 9958e0eb34 | |||
| c5269002a2 | |||
| 455a35f8ae | |||
| 0c79f207c3 | |||
| cd16d32a35 | |||
| 1989c1eb48 | |||
| f56856529f | |||
| 52e27a3e39 | |||
| 177c971b52 | |||
| 7a52e19235 | |||
| 5171e509a5 | |||
| 975a3b1828 | |||
| c11887fada | |||
| e043cb5690 | |||
| b2d5354798 | |||
| a211a4143b | |||
| c0df7d314b | |||
| c8a8ce07e2 | |||
| e0e474dfce | |||
| 7591748811 | |||
| 884308690f | |||
| 15bd5b4b7a | |||
| abc3a16ee3 | |||
| bb09ccf3c0 | |||
| ad2ea8095b | |||
| 760d1116a1 | |||
| 47fcf7eb97 | |||
| b0e90c2f63 | |||
| f502884fdd | |||
| 5ed79523d2 | |||
| da5dd70969 | |||
| 68e69085df | |||
| 640ce8f5d7 | |||
| c960cc1ee5 | |||
| 2b2601aa4a | |||
| 99a10ec7db | |||
| 035105adf0 | |||
| f983f0e359 | |||
| 769472b24c | |||
| 8c80ad7575 | |||
| 63db2e6695 | |||
| d6d5e97fbd | |||
| 1ae0a8326e | |||
| 57693fef7b | |||
| 5656016700 | |||
| 90ae180b3e | |||
| 2a3c78d43e | |||
| 11000af718 | |||
| b808121f1d | |||
| addadefeb1 | |||
| 838cd20e57 | |||
| 5b9219522d | |||
| caeb4d273d | |||
| 77cf87c989 | |||
| 50c2dbed5d | |||
| 71a9396952 | |||
| bc3ad75328 | |||
| 077bbc3c38 | |||
| b1b1abad1d | |||
| e6ba2a0066 | |||
| 9b56ef7d82 | |||
| dd442c6653 | |||
| 3044317b09 | |||
| 3a1e1e01dc | |||
| a567701639 | |||
| 1802271358 | |||
| 9e649eef79 | |||
| 1eb4a9c216 | |||
| e3f65d2192 | |||
| bb09cfddb3 | |||
| d383939c9f | |||
| 32dd543562 | |||
| 5a75f26791 | |||
| 95c437efd5 | |||
| ec877f632f | |||
| 8666cbf8bc | |||
| 84b0c26450 | |||
| 64e5bbabb3 | |||
| cc1a15e5ba | |||
| d29e942a72 | |||
| 8d86c88c38 | |||
| c7dc7421aa | |||
| 34ed3e5c68 | |||
| 1a4a8af384 | |||
| 62b1e99bbf | |||
| 1aa3b76934 | |||
| 3e53c50f64 | |||
| 430386bc84 | |||
| 30049e8152 | |||
| 34d9a7a233 | |||
| 183972475b | |||
| fd46727f8e | |||
| f6ce010aa2 | |||
| d0ff30df9f | |||
| 8e449abd67 | |||
| 2986130268 | |||
| 1c0c09f2f2 | |||
| 44100cb5b6 | |||
| cfc6e5cd2a | |||
| c067d14c2c | |||
| aded854a2b | |||
| e79d0b9dd2 | |||
| a0115d88b0 | |||
| 85ec2ed367 | |||
| bf908c4d17 | |||
| f41c5c9428 | |||
| 04837983fa | |||
| 5d484b012c | |||
| 436a8d0585 | |||
| 28cc0a6f84 | |||
| 26cc2f2c96 | |||
| 149107e749 | |||
| a74936c5f5 | |||
| ff8c8913d4 | |||
| 83426e1302 | |||
| 9cd93d467c | |||
| 257f8a5a27 | |||
| 79bab08cae | |||
| 4e699e4f5a | |||
| 1128f40bac | |||
| 53ef836326 | |||
| b8df0e89e5 | |||
| 472bfec6bf | |||
| c1b86cedd2 | |||
| 428c65f075 | |||
| 92ed48f7f6 | |||
| 13e84bc492 | |||
| 0ef86c34b7 | |||
| 7e1a4259d7 | |||
| c842c51fb6 | |||
| 6f2f228e08 | |||
| c78eaa8b96 | |||
| f9606526d2 | |||
| fe4cc9ea2c | |||
| 54d0c05fcc | |||
| 2f7df73a37 | |||
| cf19f3626b | |||
| ff2da5e59b | |||
| e03922e518 | |||
| 893fba5b8c | |||
| c1786f8e24 | |||
| a59f974537 | |||
| 7157e07328 | |||
| 954084bd82 | |||
| 0915ba40f6 | |||
| de30d55bcf | |||
| af1c34fba5 | |||
| 7b7d93786f | |||
| 7c1c504482 | |||
| 33b22fcab6 | |||
| ab0566dcba | |||
| c4f2cc7189 | |||
| 4626d99590 | |||
| 6465ca8a19 | |||
| 15b9d151df | |||
| dd1b6c86cd | |||
| 9613cda79a |
+27
-5
@@ -1,6 +1,28 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
* text=auto
|
||||
* text eol=lf
|
||||
|
||||
# Windows forced line-endings
|
||||
/.idea/* text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Gradle wrapper
|
||||
*.jar binary
|
||||
|
||||
# Binary files types
|
||||
*.webp binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.pyc binary
|
||||
*.swp binary
|
||||
*.pdf binary
|
||||
*.exe binary
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: "🐞 Bug report"
|
||||
title: "[Bug] <short description>"
|
||||
about: "Report a bug"
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app.
|
||||
- I have tried the troubleshooting guide described in `README.md`
|
||||
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
|
||||
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## Device information
|
||||
- Tachidesk version: (Example: v0.2.3-r255-win32)
|
||||
- Server Operating System: (Example: Ubuntu 20.04)
|
||||
- Server Desktop Environment: N/A or (Example: Gnome 40)
|
||||
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
|
||||
- Client Operating System: <usually the same as above Server Operating System>
|
||||
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
|
||||
|
||||
## Steps to reproduce
|
||||
1. First Step
|
||||
2. Second Step
|
||||
|
||||
### Expected behavior
|
||||
Describe what should have happened. Remove this line after you are done.
|
||||
|
||||
### Actual behavior
|
||||
Describe what happens instead. Remove this line after you are done.
|
||||
|
||||
## Other details
|
||||
Describe additional details If necessary. Remove this line after you are done.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "🌟 Feature request"
|
||||
title: "[Feature Request] <short description>"
|
||||
about: "Suggest a feature to improve the project"
|
||||
labels: "enhancement"
|
||||
---
|
||||
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app.
|
||||
- I have tried the troubleshooting guide described in `README.md`
|
||||
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
|
||||
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## What feature should be added to Tachidesk?
|
||||
Explain What the feature is and how it should work in detail. Remove this line after you are done.
|
||||
|
||||
## Why/Project's Benefit/Existing Problem
|
||||
Explain why this should be added. Remove this line after you are done.
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf preview/*.jar preview/*.zip
|
||||
|
||||
cp master/server/build/Tachidesk-*.jar preview
|
||||
cp master/server/build/Tachidesk-*.zip preview
|
||||
|
||||
cd preview
|
||||
|
||||
new_jar_build=$(ls Tachidesk-*.jar)
|
||||
echo "last jar build file name: $new_jar_build"
|
||||
|
||||
latest=$(echo $new_jar_build | sed -e's/Tachidesk-\|.jar//g')
|
||||
echo "{ \"latest\": \"$latest\" }" > index.json
|
||||
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git status
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Updated to $latest"
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
git lfs install
|
||||
#git lfs track "*.zip"
|
||||
|
||||
cp ../master/repo/* .
|
||||
new_jar_build=$(ls *.jar| tail -1)
|
||||
echo "last jar build file name: $new_jar_build"
|
||||
|
||||
new_win32_build=$(ls *.zip| tail -1)
|
||||
echo "last win32 build file name: $new_win32_build"
|
||||
|
||||
cp -f $new_jar_build Tachidesk-latest.jar
|
||||
cp -f $new_win32_build Tachidesk-latest-win32.zip
|
||||
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git status
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Update repo"
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get last commit message
|
||||
#last_commit_log=$(git log -1 --pretty=format:"%s")
|
||||
#echo "last commit log: $last_commit_log"
|
||||
#
|
||||
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
|
||||
#echo "count is: $filter_count"
|
||||
|
||||
mkdir -p repo/
|
||||
cp server/build/Tachidesk-*.jar repo/
|
||||
cp server/build/Tachidesk-*.zip repo/
|
||||
|
||||
ls repo
|
||||
pwd
|
||||
|
||||
#if [ "$filter_count" -gt 0 ]; then
|
||||
# cp server/build/Tachidesk-*.jar repo/
|
||||
# cp server/build/Tachidesk-*.zip repo/
|
||||
#fi
|
||||
@@ -1,9 +1,6 @@
|
||||
name: CI
|
||||
name: CI Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -19,7 +16,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build FatJar
|
||||
name: Build pull request
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -30,10 +27,10 @@ jobs:
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
- name: Checkout pull request
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -60,12 +57,12 @@ jobs:
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||
|
||||
- name: Build Jar and launch4j
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :server:windowsPackage --stacktrace
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
@@ -0,0 +1,88 @@
|
||||
name: CI build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check_wrapper:
|
||||
name: Validate Gradle Wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build artifacts and deploy preview
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cancel previous runs
|
||||
uses: styfle/cancel-workflow-action@0.5.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
cd master
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Download android.jar
|
||||
run: |
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
- name: make windows packages
|
||||
run: |
|
||||
cd master/scripts
|
||||
./windows32-bundler.sh
|
||||
./windows64-bundler.sh
|
||||
|
||||
- name: Checkout preview branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'Suwayomi/Tachidesk-preview'
|
||||
ref: main
|
||||
path: preview
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
|
||||
- name: Deploy preview
|
||||
run: |
|
||||
./master/.github/scripts/commit-preview.sh
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Issue closer
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.0
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
rules: |
|
||||
[
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*<short description>*",
|
||||
"message": "You did not fill out the description in the title"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||
"message": "The requested information was not filled out"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*Remove this line after you are done.*",
|
||||
"message": "The lines requesting to be removed were not removed."
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Publish
|
||||
name: CI Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build FatJar
|
||||
name: Build artifacts and release
|
||||
needs: check_wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
- name: Checkout ${{ github.ref }}
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
ref: ${{ github.ref }}
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -56,60 +56,30 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||
|
||||
- name: Build Jar and launch4j
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :server:windowsPackage --stacktrace
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
|
||||
- name: Create repo artifacts
|
||||
- name: make windows packages
|
||||
run: |
|
||||
cd master
|
||||
./.github/scripts/create-repo.sh
|
||||
cd master/scripts
|
||||
./windows32-bundler.sh
|
||||
./windows64-bundler.sh
|
||||
|
||||
- name: Upload Release
|
||||
uses: xresloader/upload-to-github-release@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: "master/repo/*"
|
||||
file: "master/server/build/*.jar;master/server/build/*.zip"
|
||||
tags: true
|
||||
draft: true
|
||||
verbose: true
|
||||
|
||||
# - name: Create Release
|
||||
# id: create_release
|
||||
# uses: actions/create-release@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# tag_name: ${{ github.ref }}
|
||||
# release_name: Release ${{ github.ref }}
|
||||
# body: |
|
||||
# Release body
|
||||
# draft: false
|
||||
# prerelease: true
|
||||
#
|
||||
# - name: Get the Ref
|
||||
# id: get-ref
|
||||
# uses: ankitvgupta/ref-to-tag-action@master
|
||||
# with:
|
||||
# ref: ${{ github.ref }}
|
||||
# head_ref: ${{ github.head_ref }}
|
||||
#
|
||||
# - name: Get the tag
|
||||
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
|
||||
#
|
||||
# - name: Upload Release
|
||||
# uses: AButler/upload-release-assets@v2.0
|
||||
# with:
|
||||
# files: 'master/repo/*'
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# release-tag: ${{ steps.get-ref.outputs.tag }}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# Ignore Gradle project-specific cache directory
|
||||
.gradle
|
||||
.idea
|
||||
gradle.properties
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
server/src/main/resources/react
|
||||
server/tmp/
|
||||
server/tachiserver-data/
|
||||
|
||||
# OpenJDK downlaods
|
||||
OpenJDK*
|
||||
@@ -1,4 +1,4 @@
|
||||
dependencies {
|
||||
// Config API
|
||||
// Config API, moved to the global build.gradle
|
||||
// implementation("com.typesafe:config:1.4.0")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
|
||||
/*
|
||||
* 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/. */
|
||||
|
||||
val ApplicationRootDir: String
|
||||
get(): String {
|
||||
return System.getProperty(
|
||||
"ir.armor.tachidesk.rootDir",
|
||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
@@ -10,49 +18,58 @@ import java.io.File
|
||||
* Manages app config.
|
||||
*/
|
||||
open class ConfigManager {
|
||||
private val generatedModules
|
||||
= mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||
val config by lazy { loadConfigs() }
|
||||
|
||||
//Public read-only view of modules
|
||||
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
||||
get() = generatedModules
|
||||
|
||||
open val configFolder: String
|
||||
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
|
||||
|
||||
val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Get a config module
|
||||
*/
|
||||
inline fun <reified T : ConfigModule> module(): T
|
||||
= loadedModules[T::class.java] as T
|
||||
inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
|
||||
|
||||
/**
|
||||
* Get a config module (Java API)
|
||||
*/
|
||||
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
|
||||
*/
|
||||
fun loadConfigs(): Config {
|
||||
val configs = mutableListOf<Config>()
|
||||
//Load reference configs
|
||||
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||
val baseConfig =
|
||||
ConfigFactory.parseMap(
|
||||
mapOf(
|
||||
"ts.server.rootDir" to ApplicationRootDir
|
||||
)
|
||||
)
|
||||
|
||||
//Load reference config
|
||||
configs += ConfigFactory.parseResources("reference.conf")
|
||||
//Load user config
|
||||
val userConfig =
|
||||
File(ApplicationRootDir, "server.conf").let {
|
||||
ConfigFactory.parseFile(it)
|
||||
}
|
||||
|
||||
//Load custom configs from dir
|
||||
File(configFolder).listFiles()?.map {
|
||||
ConfigFactory.parseFile(it)
|
||||
}?.filterNotNull()?.forEach {
|
||||
configs += it.withFallback(configs.last())
|
||||
|
||||
val config = ConfigFactory.empty()
|
||||
.withFallback(baseConfig)
|
||||
.withFallback(userConfig)
|
||||
.withFallback(compatConfig)
|
||||
.withFallback(serverConfig)
|
||||
.resolve()
|
||||
|
||||
// set log level early
|
||||
if (debugLogsEnabled(config)) {
|
||||
setLogLevel(Level.DEBUG)
|
||||
}
|
||||
|
||||
val config = configs.last().resolve()
|
||||
|
||||
logger.debug {
|
||||
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||
}
|
||||
@@ -61,7 +78,7 @@ open class ConfigManager {
|
||||
}
|
||||
|
||||
fun registerModule(module: ConfigModule) {
|
||||
generatedModules.put(module.javaClass, module)
|
||||
generatedModules[module.javaClass] = module
|
||||
}
|
||||
|
||||
fun registerModules(vararg modules: ConfigModule) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import com.typesafe.config.Config
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.Logger
|
||||
|
||||
fun setLogLevel(level: Level) {
|
||||
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
|
||||
}
|
||||
|
||||
fun debugLogsEnabled(config: Config)
|
||||
= System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||
@@ -1,35 +0,0 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import java.io.File
|
||||
|
||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip = config.getString("ip")
|
||||
val port = config.getInt("port")
|
||||
|
||||
val allowConfigChanges = config.getBoolean("allowConfigChanges")
|
||||
val enableWebUi = config.getBoolean("enableWebUi")
|
||||
val useOldWebUi = config.getBoolean("useOldWebUi")
|
||||
val prettyPrintApi = config.getBoolean("prettyPrintApi")
|
||||
// TODO Apply to operation IDs
|
||||
val disabledApiEndpoints = config.getStringList("disabledApiEndpoints").map(String::toLowerCase)
|
||||
val enabledApiEndpoints = config.getStringList("enabledApiEndpoints").map(String::toLowerCase)
|
||||
val httpInitializedPrintMessage = config.getString("httpInitializedPrintMessage")
|
||||
|
||||
val useExternalStaticFiles = config.getBoolean("useExternalStaticFiles")
|
||||
val externalStaticFilesFolder = config.getString("externalStaticFilesFolder")
|
||||
|
||||
val rootDir = registerFile(config.getString("rootDir"))
|
||||
val patchesDir = registerFile(config.getString("patchesDir"))
|
||||
|
||||
fun registerFile(file: String): File {
|
||||
return File(file).apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(config: Config)
|
||||
= ServerConfig(config.getConfig("ts.server"))
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ plugins {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
@@ -18,9 +17,7 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
// Android stub library
|
||||
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
|
||||
implementation(fileTree("lib/"))
|
||||
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
|
||||
|
||||
|
||||
// Android JAR libs
|
||||
@@ -32,22 +29,11 @@ dependencies {
|
||||
// Javassist
|
||||
compileOnly( "org.javassist:javassist:3.27.0-GA")
|
||||
|
||||
// Coroutines
|
||||
val kotlinx_coroutines_version = "1.4.2"
|
||||
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version")
|
||||
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinx_coroutines_version")
|
||||
|
||||
// XML
|
||||
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||
|
||||
// Config API
|
||||
implementation( project(":AndroidCompat:Config"))
|
||||
|
||||
// dex2jar
|
||||
// compileOnly( "dex2jar:dex-translator")
|
||||
|
||||
// APK parser
|
||||
compileOnly("net.dongliu:apk-parser:2.6.10")
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
// APK sig verifier
|
||||
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
||||
@@ -55,7 +41,11 @@ dependencies {
|
||||
// AndroidX annotations
|
||||
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
|
||||
|
||||
// compileOnly("io.reactivex:rxjava:1.3.8")
|
||||
// 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")
|
||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
||||
}
|
||||
|
||||
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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/.
|
||||
|
||||
# This is a windows only PowerShell script to create android.jar stubs
|
||||
|
||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||
if ($(Split-Path -Path (Get-Location) -Leaf) -eq "AndroidCompat" ) {
|
||||
Set-Location ..
|
||||
}
|
||||
|
||||
Write-Output "Getting required Android.jar..."
|
||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||
|
||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
|
||||
|
||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||
|
||||
[IO.File]::WriteAllBytes($android_jar, [Convert]::FromBase64String($androidEncoded))
|
||||
|
||||
# We need to remove any stub classes that we have implementations for
|
||||
Write-Output "Patching JAR..."
|
||||
|
||||
function Remove-Files-Zip($zipfile, $path)
|
||||
{
|
||||
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
|
||||
|
||||
$stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open)
|
||||
$mode = [IO.Compression.ZipArchiveMode]::Update
|
||||
$zip = New-Object IO.Compression.ZipArchive($stream, $mode)
|
||||
|
||||
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
|
||||
|
||||
$zip.Dispose()
|
||||
$stream.Close()
|
||||
$stream.Dispose()
|
||||
}
|
||||
|
||||
Write-Output "Removing org.json..."
|
||||
Remove-Files-Zip $android_jar 'org/json/*'
|
||||
|
||||
Write-Output "Removing org.apache..."
|
||||
Remove-Files-Zip $android_jar 'org/apache/*'
|
||||
|
||||
Write-Output "Removing org.w3c..."
|
||||
Remove-Files-Zip $android_jar 'org/w3c/*'
|
||||
|
||||
Write-Output "Removing org.xml..."
|
||||
Remove-Files-Zip $android_jar 'org/xml/*'
|
||||
|
||||
Write-Output "Removing org.xmlpull..."
|
||||
Remove-Files-Zip $android_jar 'org/xmlpull/*'
|
||||
|
||||
Write-Output "Removing junit..."
|
||||
Remove-Files-Zip $android_jar 'junit/*'
|
||||
|
||||
Write-Output "Removing javax..."
|
||||
Remove-Files-Zip $android_jar 'javax/*'
|
||||
|
||||
Write-Output "Removing java..."
|
||||
Remove-Files-Zip $android_jar 'java/*'
|
||||
|
||||
Write-Output "Removing overriden classes..."
|
||||
Remove-Files-Zip $android_jar 'android/app/Application.class'
|
||||
Remove-Files-Zip $android_jar 'android/app/Service.class'
|
||||
Remove-Files-Zip $android_jar 'android/net/Uri.class'
|
||||
Remove-Files-Zip $android_jar 'android/net/Uri$Builder.class'
|
||||
Remove-Files-Zip $android_jar 'android/os/Environment.class'
|
||||
Remove-Files-Zip $android_jar 'android/text/format/Formatter.class'
|
||||
Remove-Files-Zip $android_jar 'android/text/Html.class'
|
||||
|
||||
function Dedupe($path)
|
||||
{
|
||||
Push-Location $path
|
||||
$classes = Get-ChildItem . *.* -Recurse | Where-Object { !$_.PSIsContainer }
|
||||
$classes | ForEach-Object {
|
||||
"Processing class: $($_.FullName)"
|
||||
Remove-Files-Zip $android_jar "$($_.Name).class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)$*.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)Kt.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)Kt$*.class" | Out-Null
|
||||
}
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Dedupe "AndroidCompat/src/main/java"
|
||||
Dedupe "server/src/main/kotlin"
|
||||
|
||||
Write-Output "Copying Android.jar to library folder..."
|
||||
Move-Item -Force $android_jar "AndroidCompat/lib/android.jar"
|
||||
|
||||
Write-Output "Cleaning up..."
|
||||
Remove-Item -Recurse -Force "tmp"
|
||||
|
||||
Write-Output "Done!"
|
||||
@@ -1,4 +1,30 @@
|
||||
#!/usr/bin/env 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/.
|
||||
|
||||
# This is a bash script to create android.jar stubs
|
||||
|
||||
for dep in "curl" "base64" "zip"
|
||||
do
|
||||
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
|
||||
done
|
||||
|
||||
if [ $abort = yes ]; then
|
||||
echo "Some of the dependencies didn't exist. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||
if [ "$(basename "$(pwd)")" = "AndroidCompat" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
|
||||
echo "Getting required Android.jar..."
|
||||
rm -rf "tmp"
|
||||
mkdir -p "tmp"
|
||||
@@ -6,7 +32,7 @@ pushd "tmp"
|
||||
|
||||
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
|
||||
|
||||
# We need to remove any stub classes that we might use
|
||||
# We need to remove any stub classes that we have implementations for
|
||||
echo "Patching JAR..."
|
||||
|
||||
echo "Removing org.json..."
|
||||
@@ -33,7 +59,7 @@ zip --delete android.jar javax/*
|
||||
echo "Removing java..."
|
||||
zip --delete android.jar java/*
|
||||
|
||||
echo "Removing overriden classes..."
|
||||
echo "Removing overridden classes..."
|
||||
zip --delete android.jar android/app/Application.class
|
||||
zip --delete android.jar android/app/Service.class
|
||||
zip --delete android.jar android/net/Uri.class
|
||||
@@ -42,12 +68,12 @@ zip --delete android.jar android/os/Environment.class
|
||||
zip --delete android.jar android/text/format/Formatter.class
|
||||
zip --delete android.jar android/text/Html.class
|
||||
|
||||
# Dedup overriden Android classes
|
||||
# Dedup overridden Android classes
|
||||
ABS_JAR="$(realpath android.jar)"
|
||||
function dedup() {
|
||||
pushd "$1"
|
||||
CLASSES="$(find * -type f)"
|
||||
echo "$CLASSES" | while read class
|
||||
CLASSES="$(find ./* -type f)"
|
||||
echo "$CLASSES" | while read -r class
|
||||
do
|
||||
NAME="${class%.*}"
|
||||
echo "Processing class: $NAME"
|
||||
@@ -56,13 +82,10 @@ function dedup() {
|
||||
popd
|
||||
}
|
||||
|
||||
pushd ..
|
||||
popd
|
||||
dedup AndroidCompat/src/main/java
|
||||
dedup server/src/main/java
|
||||
dedup server/src/main/kotlin
|
||||
popd
|
||||
|
||||
popd
|
||||
echo "Copying Android.jar to library folder..."
|
||||
mv tmp/android.jar AndroidCompat/lib
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.duktape;
|
||||
|
||||
import kotlin.NotImplementedError;
|
||||
@@ -22,11 +23,18 @@ import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
import java.io.Closeable;
|
||||
|
||||
/** A simple EMCAScript (Javascript) interpreter. */
|
||||
/* Note (March 2021):
|
||||
* The old implementation for duktape-android used the nashorn engine which is deprecated.
|
||||
* This new implementation uses Mozilla's Rhino: https://github.com/mozilla/rhino
|
||||
*/
|
||||
|
||||
/**
|
||||
* A simple EMCAScript (Javascript) interpreter.
|
||||
*/
|
||||
public final class Duktape implements Closeable, AutoCloseable {
|
||||
|
||||
private ScriptEngineManager factory = new ScriptEngineManager();
|
||||
private ScriptEngine engine = factory.getEngineByName("JavaScript");
|
||||
private ScriptEngine engine = factory.getEngineByName("rhino");
|
||||
|
||||
/**
|
||||
* Create a new interpreter instance. Calls to this method <strong>must</strong> matched with
|
||||
@@ -38,17 +46,6 @@ public final class Duktape implements Closeable, AutoCloseable {
|
||||
|
||||
private Duktape() {}
|
||||
|
||||
/**
|
||||
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
|
||||
* reporting. Note that the result must be one of the supported Java types or the call will
|
||||
* return null.
|
||||
*
|
||||
* @throws DuktapeException if there is an error evaluating the script.
|
||||
*/
|
||||
public synchronized Object evaluate(String script, String fileName) {
|
||||
throw new NotImplementedError("Not implemented!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate {@code script} and return a result. Note that the result must be one of the
|
||||
* supported Java types or the call will return null.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.squareup.duktape;
|
||||
|
||||
/* part of tachiyomi-extensions which is licensed under Apache License Version 2.0 */
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/** This is the reference Duktape stub that tachiyomi's extensions depend on.
|
||||
* Intended to be used as a reference.
|
||||
*/
|
||||
public class DuktapeStub implements Closeable {
|
||||
|
||||
public static Duktape create() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public synchronized Object evaluate(String script) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public synchronized <T> void set(String name, Class<T> type, T object) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,7 @@ data class InstalledPackage(val root: File) {
|
||||
val icon = File(root, "icon.png")
|
||||
|
||||
val info: PackageInfo
|
||||
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(root, apk).also {
|
||||
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also {
|
||||
val parsed = ApkFile(apk)
|
||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||
val dBuilder = dbFactory.newDocumentBuilder()
|
||||
@@ -82,12 +82,14 @@ data class InstalledPackage(val root: File) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeList.toList(): List<Node> {
|
||||
val out = mutableListOf<Node>()
|
||||
companion object {
|
||||
fun NodeList.toList(): List<Node> {
|
||||
val out = mutableListOf<Node>()
|
||||
|
||||
for(i in 0 until length)
|
||||
out += item(i)
|
||||
for (i in 0 until length)
|
||||
out += item(i)
|
||||
|
||||
return out
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import android.content.pm.PackageInfo
|
||||
import net.dongliu.apk.parser.bean.ApkMeta
|
||||
import java.io.File
|
||||
|
||||
fun ApkMeta.toPackageInfo(root: File, apk: File): PackageInfo {
|
||||
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
|
||||
return PackageInfo().also {
|
||||
it.packageName = packageName
|
||||
it.versionCode = versionCode.toInt()
|
||||
|
||||
-3
@@ -1,6 +1,3 @@
|
||||
# Server ip and port bindings
|
||||
ts.server.ip = 0.0.0.0
|
||||
ts.server.port = 4567
|
||||
|
||||
# Allow/disallow preference changes (useful for demos)
|
||||
ts.server.allowConfigChanges = true
|
||||
@@ -0,0 +1,5 @@
|
||||
# Code Of Conduct
|
||||
- Don't be a dick.
|
||||
|
||||
# expanding the code of conduct!
|
||||
The contents of this document is up for debate and improvement! Discussions on discord.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
## Where should I start?
|
||||
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.
|
||||
|
||||
## How does Tachidesk work?
|
||||
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`.
|
||||
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
||||
|
||||
## Why a web app?
|
||||
This structure is chosen to
|
||||
- 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
|
||||
- Eaise development of alternative user intefaces 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
|
||||
### Prerequisites
|
||||
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.
|
||||
|
||||
@@ -1,63 +1,68 @@
|
||||
|
||||
| Build | Stable | Preview | Support Server |
|
||||
|-------|----------|---------|---------|
|
||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/tree/main/) | [](https://discord.gg/DDZdqZWaHA) |
|
||||
|
||||
# Tachidesk
|
||||
<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/).
|
||||
|
||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
||||
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.
|
||||
|
||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||
|
||||
## How do I run the app?
|
||||
#### Prerequisites
|
||||
You should have The Java Runtime Environment(JRE) 8 or newer (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||
|
||||
#### Download the app
|
||||
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||
|
||||
#### Running pre-built jar packages
|
||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||
|
||||
#### Running pre-built Windows packages
|
||||
Windows specific builds have java bundled inside them, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`, the rest will work like the jar release.
|
||||
|
||||
#### Running on Docker
|
||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||
|
||||
## Building from source
|
||||
### Get Android stubs jar
|
||||
#### Manual download
|
||||
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
||||
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
||||
### building the jar
|
||||
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building the Windows package
|
||||
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
||||
## Running for development purposes
|
||||
### `server` module
|
||||
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||
### `webUI` module
|
||||
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 client if a new browser window doesn't start 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.
|
||||
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
|
||||
|
||||
## Is this application usable? Should I test it?
|
||||
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
|
||||
Here is a list of current features:
|
||||
|
||||
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
|
||||
- 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.
|
||||
- Searching and browsing installed sources.
|
||||
- A decent chapter reader.
|
||||
- Ability to download Mangas for offline read(This partially works)
|
||||
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||
|
||||
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
|
||||
**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.
|
||||
|
||||
## How does it work?
|
||||
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`.
|
||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||
## Downloading and Running the app
|
||||
### All Operating Systems
|
||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||
|
||||
## Support
|
||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
|
||||
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
|
||||
|
||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||
|
||||
### Windows
|
||||
Download the latest win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
||||
|
||||
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win64.zip` and run `Tachidesk Launcher.exe` or `Tachidesk Launcher.bat`. The rest works like the previous section.
|
||||
|
||||
### Arch Linux
|
||||
You can install Tachidesk from the AUR
|
||||
```
|
||||
yay -S tachidesk
|
||||
```
|
||||
|
||||
### Docker
|
||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||
|
||||
### 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.
|
||||
|
||||
## Troubleshooting and Support
|
||||
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
|
||||
|
||||
## Contributing and Technical info
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
## Credit
|
||||
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||
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`.
|
||||
|
||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||
|
||||
@@ -67,7 +72,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2020-2021 Aria Moradi and contributors
|
||||
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
|
||||
|
||||
+33
-30
@@ -1,33 +1,30 @@
|
||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version "1.4.21" apply false // Also in buildSrc Config.kt
|
||||
id("java")
|
||||
kotlin("jvm") version "1.4.32"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "xyz.nulldev.ts"
|
||||
group = "ir.armor.tachidesk"
|
||||
|
||||
version = "1.0"
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven("https://maven.google.com/")
|
||||
maven("https://jitpack.io")
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||
maven("https://dl.bintray.com/inorichi/maven")
|
||||
maven("https://dl.google.com/dl/android/maven2/")
|
||||
}
|
||||
}
|
||||
|
||||
val javaProjects = listOf(
|
||||
val projects = listOf(
|
||||
project(":AndroidCompat"),
|
||||
project(":AndroidCompat:Config"),
|
||||
project(":server")
|
||||
)
|
||||
|
||||
configure(javaProjects) {
|
||||
apply(plugin = "java")
|
||||
configure(projects) {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
|
||||
java {
|
||||
@@ -35,46 +32,52 @@ configure(javaProjects) {
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Kotlin
|
||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
||||
testImplementation(kotlin("test", version = "1.4.21"))
|
||||
}
|
||||
}
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation(kotlin("reflect"))
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
// coroutines
|
||||
val coroutinesVersion = "1.4.3"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||
|
||||
configure(listOf(
|
||||
project(":AndroidCompat"),
|
||||
project(":server"),
|
||||
project(":AndroidCompat:Config")
|
||||
|
||||
)) {
|
||||
dependencies {
|
||||
// Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
|
||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
|
||||
|
||||
// Logging
|
||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||
implementation("org.slf4j:slf4j-simple:1.7.30")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||
|
||||
// RxJava
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("io.reactivex:rxkotlin:1.0.0")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
|
||||
// JSoup
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
|
||||
// Kotlin
|
||||
implementation(kotlin("reflect", version = "1.4.21"))
|
||||
|
||||
// dependency of :AndroidCompat:Config
|
||||
implementation("com.typesafe:config:1.4.0")
|
||||
implementation("com.typesafe:config:1.4.1")
|
||||
implementation("io.github.config4k:config4k:0.4.2")
|
||||
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.1")
|
||||
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
// APK parser
|
||||
implementation("net.dongliu:apk-parser:2.6.10")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
start "" jre/bin/javaw -jar Tachidesk.jar
|
||||
@@ -0,0 +1,5 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
int main() {
|
||||
system("start jre\\bin\\javaw -jar Tachidesk.jar");
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Building `Tachidesk Launcher.exe`
|
||||
1. compile `Tachidesk Launcher.c` statically with MSVC compiler.
|
||||
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright (C) Contributors to the Suwayomi project
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
Write-Output "Downloading jre..."
|
||||
|
||||
$jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||
if (!(Test-Path $jre)) {
|
||||
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
||||
}
|
||||
|
||||
Write-Output "creating windows bundle"
|
||||
|
||||
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
||||
$release_name=$jar.BaseName + "-win32"
|
||||
|
||||
# make release dir
|
||||
New-Item -ItemType Directory $release_name
|
||||
|
||||
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
||||
|
||||
# move jre
|
||||
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
||||
|
||||
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
||||
|
||||
Copy-Item "resources/Tachidesk Launcher-win32.exe" $release_name
|
||||
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
||||
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||
|
||||
$zip_name="$release_name.zip"
|
||||
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Remove-Item -Force -Recurse $release_name
|
||||
|
||||
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) Contributors to the Suwayomi project
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
echo "Downloading jre..."
|
||||
|
||||
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||
if [ ! -f $jre ]; then
|
||||
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -o $jre
|
||||
fi
|
||||
|
||||
echo "creating windows bundle"
|
||||
|
||||
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win32
|
||||
|
||||
# make release dir
|
||||
mkdir $release_name
|
||||
|
||||
unzip $jre
|
||||
|
||||
# move jre
|
||||
mv jdk8u292-b10-jre $release_name/jre
|
||||
|
||||
cp $jar $release_name/Tachidesk.jar
|
||||
|
||||
cp "resources/Tachidesk Launcher-win32.exe" "$release_name/Tachidesk Launcher.exe"
|
||||
cp "resources/Tachidesk Launcher.bat" $release_name
|
||||
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||
|
||||
zip_name=$release_name.zip
|
||||
zip -9 -r $zip_name $release_name
|
||||
|
||||
rm -rf $release_name
|
||||
|
||||
mv $zip_name ../server/build/
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright (C) Contributors to the Suwayomi project
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
Write-Output "Downloading jre..."
|
||||
|
||||
$jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||
if (!(Test-Path $jre)) {
|
||||
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
||||
}
|
||||
|
||||
Write-Output "creating windows bundle"
|
||||
|
||||
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
||||
$release_name=$jar.BaseName + "-win64"
|
||||
|
||||
# make release dir
|
||||
New-Item -ItemType Directory $release_name
|
||||
|
||||
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
||||
|
||||
# move jre
|
||||
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
||||
|
||||
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
||||
|
||||
Copy-Item "resources/Tachidesk Launcher-win64.exe" $release_name
|
||||
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
||||
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||
|
||||
$zip_name="$release_name.zip"
|
||||
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Remove-Item -Force -Recurse $release_name
|
||||
|
||||
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) Contributors to the Suwayomi project
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
echo "Downloading jre..."
|
||||
|
||||
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||
if [ ! -f $jre ]; then
|
||||
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
|
||||
fi
|
||||
|
||||
echo "creating windows bundle"
|
||||
|
||||
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
|
||||
|
||||
# make release dir
|
||||
mkdir $release_name
|
||||
|
||||
unzip $jre
|
||||
|
||||
# move jre
|
||||
mv jdk8u292-b10-jre $release_name/jre
|
||||
|
||||
cp $jar $release_name/Tachidesk.jar
|
||||
|
||||
cp "resources/Tachidesk Launcher-win64.exe" "$release_name/Tachidesk Launcher.exe"
|
||||
cp "resources/Tachidesk Launcher.bat" $release_name
|
||||
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||
|
||||
zip_name=$release_name.zip
|
||||
zip -9 -r $zip_name $release_name
|
||||
|
||||
rm -rf $release_name
|
||||
|
||||
mv $zip_name ../server/build/
|
||||
+113
-160
@@ -1,106 +1,80 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
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
|
||||
|
||||
plugins {
|
||||
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
id("org.jmailen.kotlinter") version "3.3.0"
|
||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||
id("org.jmailen.kotlinter") version "3.4.3"
|
||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||
}
|
||||
|
||||
val TachideskVersion = "v0.2.2"
|
||||
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url = uri("https://repo1.maven.org/maven2/")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
// okhttp
|
||||
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
// Javalin api
|
||||
implementation("io.javalin:javalin:3.13.6")
|
||||
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
|
||||
|
||||
// Exposed ORM
|
||||
val exposedVersion = "0.31.1"
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||
// current database driver
|
||||
implementation("com.h2database:h2:1.4.200")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:4.1")
|
||||
implementation("com.dorkbox:Utilities:1.9")
|
||||
|
||||
|
||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
val okhttp_version = "4.10.0-RC1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version")
|
||||
implementation("com.squareup.okio:okio:2.9.0")
|
||||
|
||||
|
||||
// retrofit
|
||||
val retrofit_version = "2.9.0"
|
||||
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
|
||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
|
||||
|
||||
|
||||
// reactivex
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
// implementation("io.reactivex:rxandroid:1.2.1")
|
||||
// implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
// implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||
|
||||
|
||||
val coroutinesVersion = "1.3.9"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
|
||||
// dex2jar
|
||||
implementation(fileTree("lib/dex2jar/"))
|
||||
|
||||
// api
|
||||
implementation("io.javalin:javalin:3.12.0")
|
||||
implementation("org.slf4j:slf4j-simple:1.8.0-beta4")
|
||||
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.0")
|
||||
|
||||
// Exposed ORM
|
||||
val exposed_version = "0.28.1"
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||
implementation("com.h2database:h2:1.4.199")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:3.17")
|
||||
|
||||
|
||||
// AndroidCompat
|
||||
implementation(project(":AndroidCompat"))
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
// uncomment to test extensions directly
|
||||
// implementation(fileTree("lib/"))
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
// Testing
|
||||
testImplementation(kotlin("test-junit5"))
|
||||
}
|
||||
|
||||
val name = "ir.armor.tachidesk.Main"
|
||||
val MainClass = "ir.armor.tachidesk.MainKt"
|
||||
application {
|
||||
mainClass.set(name)
|
||||
|
||||
// Required by ShadowJar.
|
||||
mainClassName = name
|
||||
mainClass.set(MainClass)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -111,9 +85,13 @@ sourceSets {
|
||||
}
|
||||
}
|
||||
|
||||
val TachideskRevision = Runtime
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = "v0.3.8"
|
||||
|
||||
// counts commit count on master
|
||||
val tachideskRevision = Runtime
|
||||
.getRuntime()
|
||||
.exec("git rev-list master --count")
|
||||
.exec("git rev-list HEAD --count")
|
||||
.let { process ->
|
||||
process.waitFor()
|
||||
val output = process.inputStream.use {
|
||||
@@ -124,94 +102,69 @@ val TachideskRevision = Runtime
|
||||
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
appName = rootProject.name
|
||||
clsName = "BuildConfig"
|
||||
packageName = "ir.armor.tachidesk.server"
|
||||
version = tachideskVersion
|
||||
|
||||
|
||||
buildConfigField("String", "name", rootProject.name) // alias for BuildConfig.NAME
|
||||
buildConfigField("String", "version", tachideskVersion) // alias for BuildConfig.VERSION
|
||||
buildConfigField("String", "revision", tachideskRevision)
|
||||
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
||||
}
|
||||
|
||||
tasks {
|
||||
jar {
|
||||
shadowJar {
|
||||
manifest {
|
||||
attributes(
|
||||
mapOf(
|
||||
"Main-Class" to "com.example.MainKt", //will make your jar (produced by jar task) runnable
|
||||
"ImplementationTitle" to project.name,
|
||||
"Implementation-Version" to project.version)
|
||||
"Main-Class" to MainClass,
|
||||
"Implementation-Title" to rootProject.name,
|
||||
"Implementation-Vendor" to "The Suwayomi Project",
|
||||
"Specification-Version" to tachideskVersion,
|
||||
"Implementation-Version" to tachideskRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
archiveBaseName.set(rootProject.name)
|
||||
archiveVersion.set(tachideskVersion)
|
||||
archiveClassifier.set(tachideskRevision)
|
||||
}
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
shadowJar {
|
||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
||||
archiveBaseName.set("Tachidesk")
|
||||
archiveVersion.set(TachideskVersion)
|
||||
archiveClassifier.set(TachideskRevision)
|
||||
|
||||
test {
|
||||
useJUnit()
|
||||
}
|
||||
|
||||
withType<ShadowJar> {
|
||||
destinationDirectory.set(File("$rootDir/server/build"))
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
named<Copy>("processResources") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
mustRunAfter(":webUI:copyBuild")
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
|
||||
withType<FormatTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
}
|
||||
|
||||
launch4j { //used for windows
|
||||
mainClassName = name
|
||||
bundledJrePath = "jre"
|
||||
bundledJre64Bit = true
|
||||
jreMinVersion = "8"
|
||||
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||
icon = "${projectDir}/src/main/resources/icon/icon_round.ico"
|
||||
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
||||
}
|
||||
|
||||
tasks.register<Zip>("windowsPackage") {
|
||||
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
|
||||
destinationDirectory.set(File("$buildDir"))
|
||||
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
|
||||
dependsOn("windowsPackageWorkaround2")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("windowsPackageWorkaround2") {
|
||||
delete(
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||
)
|
||||
dependsOn("windowsPackageWorkaround")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("windowsPackageWorkaround") {
|
||||
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
dependsOn("deleteUnwantedJreDir")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("deleteUnwantedJreDir") {
|
||||
delete(
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
|
||||
)
|
||||
dependsOn("addJreToDistributable")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("addJreToDistributable") {
|
||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
eachFile {
|
||||
path = path.replace(".*-jre".toRegex(),"jre")
|
||||
}
|
||||
dependsOn("downloadJre")
|
||||
dependsOn("createExe")
|
||||
}
|
||||
|
||||
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
||||
dest(buildDir)
|
||||
overwrite(false)
|
||||
onlyIfModified(true)
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
destinationDir = File("$rootDir/server/build")
|
||||
dependsOn("lintKotlin")
|
||||
}
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(":webUI:copyBuild")
|
||||
}
|
||||
|
||||
tasks.named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,252 +0,0 @@
|
||||
package ir.armor.tachidesk;
|
||||
|
||||
/* 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 org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class APKExtractor {
|
||||
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
|
||||
// such as for AndroidManifest.xml in .apk files
|
||||
public static int endDocTag = 0x00100101;
|
||||
public static int startTag = 0x00100102;
|
||||
public static int endTag = 0x00100103;
|
||||
|
||||
static void prt(String str) {
|
||||
//System.err.print(str);
|
||||
}
|
||||
|
||||
public static String decompressXML(byte[] xml) {
|
||||
|
||||
StringBuilder finalXML = new StringBuilder();
|
||||
|
||||
// Compressed XML file/bytes starts with 24x bytes of data,
|
||||
// 9 32 bit words in little endian order (LSB first):
|
||||
// 0th word is 03 00 08 00
|
||||
// 3rd word SEEMS TO BE: Offset at then of StringTable
|
||||
// 4th word is: Number of strings in string table
|
||||
// WARNING: Sometime I indiscriminently display or refer to word in
|
||||
// little endian storage format, or in integer format (ie MSB first).
|
||||
int numbStrings = LEW(xml, 4 * 4);
|
||||
|
||||
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
|
||||
// of the length/string data in the StringTable.
|
||||
int sitOff = 0x24; // Offset of start of StringIndexTable
|
||||
|
||||
// StringTable, each string is represented with a 16 bit little endian
|
||||
// character count, followed by that number of 16 bit (LE) (Unicode)
|
||||
// chars.
|
||||
int stOff = sitOff + numbStrings * 4; // StringTable follows
|
||||
// StrIndexTable
|
||||
|
||||
// XMLTags, The XML tag tree starts after some unknown content after the
|
||||
// StringTable. There is some unknown data after the StringTable, scan
|
||||
// forward from this point to the flag for the start of an XML start
|
||||
// tag.
|
||||
int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
|
||||
// word.
|
||||
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
|
||||
// int)
|
||||
for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
|
||||
if (LEW(xml, ii) == startTag) {
|
||||
xmlTagOff = ii;
|
||||
break;
|
||||
}
|
||||
} // end of hack, scanning for start of first start tag
|
||||
|
||||
// XML tags and attributes:
|
||||
// Every XML start and end tag consists of 6 32 bit words:
|
||||
// 0th word: 02011000 for startTag and 03011000 for endTag
|
||||
// 1st word: a flag?, like 38000000
|
||||
// 2nd word: Line of where this tag appeared in the original source file
|
||||
// 3rd word: FFFFFFFF ??
|
||||
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
|
||||
// 5th word: StringIndex of Element Name
|
||||
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
|
||||
|
||||
// Start tags (not end tags) contain 3 more words:
|
||||
// 6th word: 14001400 meaning??
|
||||
// 7th word: Number of Attributes that follow this tag(follow word 8th)
|
||||
// 8th word: 00000000 meaning??
|
||||
|
||||
// Attributes consist of 5 words:
|
||||
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
|
||||
// 1st word: StringIndex of Attribute Name
|
||||
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
|
||||
// used
|
||||
// 3rd word: Flags?
|
||||
// 4th word: str ind of attr value again, or ResourceId of value
|
||||
|
||||
// TMP, dump string table to tr for debugging
|
||||
// tr.addSelect("strings", null);
|
||||
// for (int ii=0; ii<numbStrings; ii++) {
|
||||
// // Length of string starts at StringTable plus offset in StrIndTable
|
||||
// String str = compXmlString(xml, sitOff, stOff, ii);
|
||||
// tr.add(String.valueOf(ii), str);
|
||||
// }
|
||||
// tr.parent();
|
||||
|
||||
// Step through the XML tree element tags and attributes
|
||||
int off = xmlTagOff;
|
||||
int indent = 0;
|
||||
int startTagLineNo = -2;
|
||||
while (off < xml.length) {
|
||||
int tag0 = LEW(xml, off);
|
||||
// int tag1 = LEW(xml, off+1*4);
|
||||
int lineNo = LEW(xml, off + 2 * 4);
|
||||
// int tag3 = LEW(xml, off+3*4);
|
||||
int nameNsSi = LEW(xml, off + 4 * 4);
|
||||
int nameSi = LEW(xml, off + 5 * 4);
|
||||
|
||||
if (tag0 == startTag) { // XML START TAG
|
||||
int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
|
||||
int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
|
||||
// to follow
|
||||
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
|
||||
off += 9 * 4; // Skip over 6+3 words of startTag data
|
||||
String name = compXmlString(xml, sitOff, stOff, nameSi);
|
||||
// tr.addSelect(name, null);
|
||||
startTagLineNo = lineNo;
|
||||
|
||||
// Look for the Attributes
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (int ii = 0; ii < numbAttrs; ii++) {
|
||||
int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
|
||||
// Ind, or FFFFFFFF
|
||||
int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
|
||||
// Index
|
||||
int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
|
||||
// Ind, or
|
||||
// FFFFFFFF
|
||||
int attrFlags = LEW(xml, off + 3 * 4);
|
||||
int attrResId = LEW(xml, off + 4 * 4); // AttrValue
|
||||
// ResourceId or dup
|
||||
// AttrValue StrInd
|
||||
off += 5 * 4; // Skip over the 5 words of an attribute
|
||||
|
||||
String attrName = compXmlString(xml, sitOff, stOff,
|
||||
attrNameSi);
|
||||
String attrValue = attrValueSi != -1 ? compXmlString(xml,
|
||||
sitOff, stOff, attrValueSi) : "resourceID 0x"
|
||||
+ Integer.toHexString(attrResId);
|
||||
sb.append(" " + attrName + "=\"" + attrValue + "\"");
|
||||
// tr.add(attrName, attrValue);
|
||||
}
|
||||
finalXML.append("<" + name + sb + ">");
|
||||
prtIndent(indent, "<" + name + sb + ">");
|
||||
indent++;
|
||||
|
||||
} else if (tag0 == endTag) { // XML END TAG
|
||||
indent--;
|
||||
off += 6 * 4; // Skip over 6 words of endTag data
|
||||
String name = compXmlString(xml, sitOff, stOff, nameSi);
|
||||
finalXML.append("</" + name + ">");
|
||||
prtIndent(indent, "</" + name + "> (line " + startTagLineNo
|
||||
+ "-" + lineNo + ")");
|
||||
// tr.parent(); // Step back up the NobTree
|
||||
|
||||
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
|
||||
break;
|
||||
|
||||
} else {
|
||||
prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
|
||||
+ "' at offset " + off);
|
||||
break;
|
||||
}
|
||||
} // end of while loop scanning tags and attributes of XML tree
|
||||
//prt(" end at offset " + off);
|
||||
return finalXML.toString();
|
||||
} // end of decompressXML
|
||||
|
||||
public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
|
||||
if (strInd < 0)
|
||||
return null;
|
||||
int strOff = stOff + LEW(xml, sitOff + strInd * 4);
|
||||
return compXmlStringAt(xml, strOff);
|
||||
}
|
||||
|
||||
public static String spaces = " ";
|
||||
|
||||
public static void prtIndent(int indent, String str) {
|
||||
prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
|
||||
}
|
||||
|
||||
// compXmlStringAt -- Return the string stored in StringTable format at
|
||||
// offset strOff. This offset points to the 16 bit string length, which
|
||||
// is followed by that number of 16 bit (Unicode) chars.
|
||||
public static String compXmlStringAt(byte[] arr, int strOff) {
|
||||
int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
|
||||
byte[] chars = new byte[strLen];
|
||||
for (int ii = 0; ii < strLen; ii++) {
|
||||
chars[ii] = arr[strOff + 2 + ii * 2];
|
||||
}
|
||||
return new String(chars); // Hack, just use 8 byte chars
|
||||
} // end of compXmlStringAt
|
||||
|
||||
// LEW -- Return value of a Little Endian 32 bit word from the byte array
|
||||
// at offset off.
|
||||
public static int LEW(byte[] arr, int off) {
|
||||
return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000
|
||||
| arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF;
|
||||
} // end of LEW
|
||||
|
||||
public static Document loadXMLFromString(String xml) throws Exception {
|
||||
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
|
||||
return docBuilder.parse(new InputSource(new StringReader(xml)));
|
||||
}
|
||||
|
||||
public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
|
||||
ZipFile zip = null;
|
||||
|
||||
zip = new ZipFile(filePath);
|
||||
ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
|
||||
ZipEntry classesDex = zip.getEntry("classes.dex");
|
||||
|
||||
// write dex file
|
||||
InputStream dexStream = zip.getInputStream(classesDex);
|
||||
try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = dexStream.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
// read xml file
|
||||
InputStream is = zip.getInputStream(androidManifest);
|
||||
byte[] buf = new byte[1024000]; // 100 kb
|
||||
is.read(buf);
|
||||
is.close();
|
||||
zip.close();
|
||||
|
||||
String xml = APKExtractor.decompressXML(buf);
|
||||
try {
|
||||
Document xmlDoc = loadXMLFromString(xml);
|
||||
String pkg = xmlDoc.getDocumentElement().getAttribute("package");
|
||||
NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
NamedNodeMap attributes = nodes.item(i).getAttributes();
|
||||
System.out.println(attributes.getNamedItem("name").getNodeValue());
|
||||
if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
|
||||
return pkg + attributes.getNamedItem("value").getNodeValue();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
/*
|
||||
* 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.app.Application
|
||||
import android.content.Context
|
||||
// import android.content.res.Configuration
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
/*
|
||||
* 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.app.Application
|
||||
import com.google.gson.Gson
|
||||
// import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
// import android.content.Context
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
// import kotlinx.coroutines.Dispatchers
|
||||
// import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||
|
||||
val response = service.getRepo()
|
||||
return parseResponse(response)
|
||||
}
|
||||
|
||||
// suspend fun checkForUpdates(): List<Extension.Installed> {
|
||||
// val extensions = fin dExtensions()
|
||||
//
|
||||
// // preferences.lastExtCheck().set(Date().time)
|
||||
//
|
||||
// val installedExtensions = ExtensionLoader.loadExtensions(context)
|
||||
// .filterIsInstance<LoadResult.Success>()
|
||||
// .map { it.extension }
|
||||
//
|
||||
// val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
||||
// for (installedExt in installedExtensions) {
|
||||
// val pkgName = installedExt.pkgName
|
||||
// val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
|
||||
//
|
||||
// val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
||||
// if (hasUpdate) {
|
||||
// extensionsWithUpdate.add(installedExt)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return extensionsWithUpdate
|
||||
// }
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
return json
|
||||
.filter { element ->
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map { element ->
|
||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Used to get the extension repo listing from GitHub.
|
||||
*/
|
||||
interface ExtensionGithubService {
|
||||
|
||||
companion object {
|
||||
private val client by lazy {
|
||||
val network: NetworkHelper by injectLazy()
|
||||
network.client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.header("Content-Encoding", "gzip")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun create(): ExtensionGithubService {
|
||||
val adapter = Retrofit.Builder()
|
||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
||||
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
||||
.client(client)
|
||||
.build()
|
||||
|
||||
return adapter.create(ExtensionGithubService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}/index.json.gz")
|
||||
suspend fun getRepo(): JsonArray
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
sealed class Extension {
|
||||
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Int
|
||||
abstract val lang: String?
|
||||
abstract val isNsfw: Boolean
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val sources: List<Source>,
|
||||
val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
val isUnofficial: Boolean = false
|
||||
) : Extension()
|
||||
|
||||
data class Available(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String
|
||||
) : Extension()
|
||||
|
||||
data class Untrusted(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
val signatureHash: String,
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false
|
||||
) : Extension()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
enum class InstallStep {
|
||||
Pending, Downloading, Installing, Installed, Error;
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return this == Installed || this == Error
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
sealed class LoadResult {
|
||||
|
||||
class Success(val extension: Extension.Installed) : LoadResult()
|
||||
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
|
||||
class Error(val message: String? = null) : LoadResult() {
|
||||
constructor(exception: Throwable) : this(exception.message)
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
// import android.annotation.SuppressLint
|
||||
// import android.content.Context
|
||||
// import android.content.pm.PackageInfo
|
||||
// import android.content.pm.PackageManager
|
||||
// import dalvik.system.PathClassLoader
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
// import eu.kanade.tachiyomi.util.lang.Hash
|
||||
// import kotlinx.coroutines.async
|
||||
// import kotlinx.coroutines.runBlocking
|
||||
// import timber.log.Timber
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
*/
|
||||
// @SuppressLint("PackageManagerGetSignatures")
|
||||
internal object ExtensionLoader {
|
||||
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
// private val allowNsfwSource by lazy {
|
||||
// preferences.allowNsfwSource().get()
|
||||
// }
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
const val LIB_VERSION_MIN = 1.2
|
||||
const val LIB_VERSION_MAX = 1.2
|
||||
|
||||
// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||
|
||||
// inorichi's key
|
||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
// var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
// fun loadExtensions(context: Context): List<LoadResult> {
|
||||
// val pkgManager = context.packageManager
|
||||
// val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
||||
// val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
||||
//
|
||||
// if (extPkgs.isEmpty()) return emptyList()
|
||||
//
|
||||
// // Load each extension concurrently and wait for completion
|
||||
// return runBlocking {
|
||||
// val deferred = extPkgs.map {
|
||||
// async { loadExtension(context, it.packageName, it) }
|
||||
// }
|
||||
// deferred.map { it.await() }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Attempts to load an extension from the given package name. It checks if the extension
|
||||
* contains the required feature flag before trying to load it.
|
||||
*/
|
||||
// fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
|
||||
// val pkgInfo = try {
|
||||
// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||
// } catch (error: PackageManager.NameNotFoundException) {
|
||||
// // Unlikely, but the package may have been uninstalled at this point
|
||||
// return LoadResult.Error(error)
|
||||
// }
|
||||
// if (!isPackageAnExtension(pkgInfo)) {
|
||||
// return LoadResult.Error("Tried to load a package that wasn't a extension")
|
||||
// }
|
||||
// return loadExtension(context, pkgName, pkgInfo)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Loads an extension given its package name.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param pkgName The package name of the extension to load.
|
||||
* @param pkgInfo The package info of the extension.
|
||||
*/
|
||||
// private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
|
||||
// val pkgManager = context.packageManager
|
||||
//
|
||||
// val appInfo = try {
|
||||
// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
// } catch (error: PackageManager.NameNotFoundException) {
|
||||
// // Unlikely, but the package may have been uninstalled at this point
|
||||
// return LoadResult.Error(error)
|
||||
// }
|
||||
//
|
||||
// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||
// val versionName = pkgInfo.versionName
|
||||
// val versionCode = pkgInfo.versionCode
|
||||
//
|
||||
// if (versionName.isNullOrEmpty()) {
|
||||
// val exception = Exception("Missing versionName for extension $extName")
|
||||
// Timber.w(exception)
|
||||
// return LoadResult.Error(exception)
|
||||
// }
|
||||
//
|
||||
// // Validate lib version
|
||||
// val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||
// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
// val exception = Exception(
|
||||
// "Lib version is $libVersion, while only versions " +
|
||||
// "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||
// )
|
||||
// Timber.w(exception)
|
||||
// return LoadResult.Error(exception)
|
||||
// }
|
||||
//
|
||||
// val signatureHash = getSignatureHash(pkgInfo)
|
||||
//
|
||||
// if (signatureHash == null) {
|
||||
// return LoadResult.Error("Package $pkgName isn't signed")
|
||||
// } else if (signatureHash !in trustedSignatures) {
|
||||
// val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
||||
// Timber.w("Extension $pkgName isn't trusted")
|
||||
// return LoadResult.Untrusted(extension)
|
||||
// }
|
||||
//
|
||||
// val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
||||
// return LoadResult.Error("NSFW extension $pkgName not allowed")
|
||||
// }
|
||||
//
|
||||
// val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
//
|
||||
// val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||
// .split(";")
|
||||
// .map {
|
||||
// val sourceClass = it.trim()
|
||||
// if (sourceClass.startsWith(".")) {
|
||||
// pkgInfo.packageName + sourceClass
|
||||
// } else {
|
||||
// sourceClass
|
||||
// }
|
||||
// }
|
||||
// .flatMap {
|
||||
// try {
|
||||
// when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||
// is Source -> listOf(obj)
|
||||
// is SourceFactory -> {
|
||||
// if (isSourceNsfw(obj)) {
|
||||
// emptyList()
|
||||
// } else {
|
||||
// obj.createSources()
|
||||
// }
|
||||
// }
|
||||
// else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
// }
|
||||
// } catch (e: Throwable) {
|
||||
// Timber.e(e, "Extension load error: $extName.")
|
||||
// return LoadResult.Error(e)
|
||||
// }
|
||||
// }
|
||||
// .filter { !isSourceNsfw(it) }
|
||||
//
|
||||
// val langs = sources.filterIsInstance<CatalogueSource>()
|
||||
// .map { it.lang }
|
||||
// .toSet()
|
||||
// val lang = when (langs.size) {
|
||||
// 0 -> ""
|
||||
// 1 -> langs.first()
|
||||
// else -> "all"
|
||||
// }
|
||||
//
|
||||
// val extension = Extension.Installed(
|
||||
// extName,
|
||||
// pkgName,
|
||||
// versionName,
|
||||
// versionCode,
|
||||
// lang,
|
||||
// isNsfw,
|
||||
// sources,
|
||||
// isUnofficial = signatureHash != officialSignature
|
||||
// )
|
||||
// return LoadResult.Success(extension)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Returns true if the given package is an extension.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
// private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
||||
// return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Returns the signature hash of the package or null if it's not signed.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
// private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||
// val signatures = pkgInfo.signatures
|
||||
// return if (signatures != null && signatures.isNotEmpty()) {
|
||||
// Hash.sha256(signatures.first().toByteArray())
|
||||
// } else {
|
||||
// null
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
||||
*/
|
||||
// private fun isSourceNsfw(clazz: Any): Boolean {
|
||||
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// if (clazz !is Source && clazz !is SourceFactory) {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// // Annotations are proxied, hence this janky way of checking for them
|
||||
// return clazz.javaClass.annotations
|
||||
// .flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
||||
// .firstOrNull { it == Nsfw::class.java.simpleName } != null
|
||||
// }
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
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
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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 okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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.content.Context
|
||||
// import eu.kanade.tachiyomi.BuildConfig
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
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()
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
object Hash {
|
||||
|
||||
private val chars = charArrayOf(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f'
|
||||
)
|
||||
|
||||
private val MD5 get() = MessageDigest.getInstance("MD5")
|
||||
|
||||
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
fun sha256(bytes: ByteArray): String {
|
||||
return encodeHex(SHA256.digest(bytes))
|
||||
}
|
||||
|
||||
fun sha256(string: String): String {
|
||||
return sha256(string.toByteArray())
|
||||
}
|
||||
|
||||
fun md5(bytes: ByteArray): String {
|
||||
return encodeHex(MD5.digest(bytes))
|
||||
}
|
||||
|
||||
fun md5(string: String): String {
|
||||
return md5(string.toByteArray())
|
||||
}
|
||||
|
||||
private fun encodeHex(data: ByteArray): String {
|
||||
val l = data.size
|
||||
val out = CharArray(l shl 1)
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (i < l) {
|
||||
out[j++] = chars[(240 and data[i].toInt()).ushr(4)]
|
||||
out[j++] = chars[15 and data[i].toInt()]
|
||||
i++
|
||||
}
|
||||
return String(out)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
/* 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 net.harawata.appdirs.AppDirsFactory
|
||||
|
||||
object Config {
|
||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||
val extensionsRoot = "$dataRoot/extensions"
|
||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||
val mangaRoot = "$dataRoot/manga"
|
||||
}
|
||||
@@ -1,286 +1,16 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.App
|
||||
import io.javalin.Javalin
|
||||
import ir.armor.tachidesk.util.addMangaToCategory
|
||||
import ir.armor.tachidesk.util.addMangaToLibrary
|
||||
import ir.armor.tachidesk.util.applicationSetup
|
||||
import ir.armor.tachidesk.util.createCategory
|
||||
import ir.armor.tachidesk.util.getCategoryList
|
||||
import ir.armor.tachidesk.util.getCategoryMangaList
|
||||
import ir.armor.tachidesk.util.getChapter
|
||||
import ir.armor.tachidesk.util.getChapterList
|
||||
import ir.armor.tachidesk.util.getExtensionIcon
|
||||
import ir.armor.tachidesk.util.getExtensionList
|
||||
import ir.armor.tachidesk.util.getLibraryMangas
|
||||
import ir.armor.tachidesk.util.getManga
|
||||
import ir.armor.tachidesk.util.getMangaCategories
|
||||
import ir.armor.tachidesk.util.getMangaList
|
||||
import ir.armor.tachidesk.util.getPageImage
|
||||
import ir.armor.tachidesk.util.getSource
|
||||
import ir.armor.tachidesk.util.getSourceList
|
||||
import ir.armor.tachidesk.util.getThumbnail
|
||||
import ir.armor.tachidesk.util.installAPK
|
||||
import ir.armor.tachidesk.util.openInBrowser
|
||||
import ir.armor.tachidesk.util.removeCategory
|
||||
import ir.armor.tachidesk.util.removeExtension
|
||||
import ir.armor.tachidesk.util.removeMangaFromCategory
|
||||
import ir.armor.tachidesk.util.removeMangaFromLibrary
|
||||
import ir.armor.tachidesk.util.reorderCategory
|
||||
import ir.armor.tachidesk.util.sourceFilters
|
||||
import ir.armor.tachidesk.util.sourceGlobalSearch
|
||||
import ir.armor.tachidesk.util.sourceSearch
|
||||
import ir.armor.tachidesk.util.systemTray
|
||||
import ir.armor.tachidesk.util.updateCategory
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import xyz.nulldev.androidcompat.AndroidCompat
|
||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
||||
import ir.armor.tachidesk.server.applicationSetup
|
||||
|
||||
class Main {
|
||||
companion object {
|
||||
val androidCompat by lazy { AndroidCompat() }
|
||||
|
||||
fun registerConfigModules() {
|
||||
GlobalConfigManager.registerModules(
|
||||
// ServerConfig.register(GlobalConfigManager.config),
|
||||
// SyncConfigModule.register(GlobalConfigManager.config)
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
// System.getProperties()["proxySet"] = "true"
|
||||
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
|
||||
// System.getProperties()["socksProxyPort"] = "2020"
|
||||
|
||||
// make sure everything we need exists
|
||||
applicationSetup()
|
||||
val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
|
||||
|
||||
registerConfigModules()
|
||||
|
||||
// Load config API
|
||||
DI.global.addImport(ConfigKodeinModule().create())
|
||||
// Load Android compatibility dependencies
|
||||
AndroidCompatInitializer().init()
|
||||
// start app
|
||||
androidCompat.startApp(App())
|
||||
|
||||
var hasWebUiBundled: Boolean = false
|
||||
|
||||
val app = Javalin.create { config ->
|
||||
try {
|
||||
this::class.java.classLoader.getResource("/react/index.html")
|
||||
hasWebUiBundled = true
|
||||
config.addStaticFiles("/react")
|
||||
config.addSinglePageRoot("/", "/react/index.html")
|
||||
} catch (e: RuntimeException) {
|
||||
println("Warning: react build files are missing.")
|
||||
hasWebUiBundled = false
|
||||
}
|
||||
config.enableCorsForAllOrigins()
|
||||
}.start(4567)
|
||||
if (hasWebUiBundled) {
|
||||
openInBrowser()
|
||||
}
|
||||
|
||||
// app.before() { ctx ->
|
||||
// // allow the client which is running on another port
|
||||
// ctx.header("Access-Control-Allow-Origin", "*")
|
||||
// }
|
||||
|
||||
app.get("/api/v1/extension/list") { ctx ->
|
||||
ctx.json(getExtensionList())
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
println("installing $apkName")
|
||||
|
||||
ctx.status(
|
||||
installAPK(apkName)
|
||||
)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
println("uninstalling $apkName")
|
||||
removeExtension(apkName)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
val result = getExtensionIcon(apkName)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
app.get("/api/v1/source/list") { ctx ->
|
||||
ctx.json(getSourceList())
|
||||
}
|
||||
|
||||
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSource(sourceId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||
}
|
||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getManga(mangaId))
|
||||
}
|
||||
|
||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val result = getThumbnail(mangaId)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// adds the manga to library
|
||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
addMangaToLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the library
|
||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
removeMangaFromLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getMangaCategories(mangaId))
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
addMangaToCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the category
|
||||
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeMangaFromCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapterList(mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapter(chapterId, mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
val result = getPageImage(mangaId, chapterId, index)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// global search
|
||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(sourceGlobalSearch(searchTerm))
|
||||
}
|
||||
|
||||
// single source search
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
||||
}
|
||||
|
||||
// source filter list
|
||||
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
// lists mangas that have no category assigned
|
||||
app.get("/api/v1/library/") { ctx ->
|
||||
ctx.json(getLibraryMangas())
|
||||
}
|
||||
|
||||
// category list
|
||||
app.get("/api/v1/category/") { ctx ->
|
||||
ctx.json(getCategoryList())
|
||||
}
|
||||
|
||||
// category create
|
||||
app.post("/api/v1/category/") { ctx ->
|
||||
val name = ctx.formParam("name")!!
|
||||
createCategory(name)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category modification
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId")!!.toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||
updateCategory(categoryId, name, isLanding)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category re-ordering
|
||||
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val from = ctx.formParam("from")!!.toInt()
|
||||
val to = ctx.formParam("to")!!.toInt()
|
||||
reorderCategory(categoryId, from, to)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category delete
|
||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeCategory(categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// returns the manga list associated with a category
|
||||
app.get("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
ctx.json(getCategoryMangaList(categoryId))
|
||||
}
|
||||
}
|
||||
}
|
||||
fun main() {
|
||||
applicationSetup()
|
||||
javalinSetup()
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package ir.armor.tachidesk.database
|
||||
|
||||
/* 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 ir.armor.tachidesk.Config
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object DBMangaer {
|
||||
val db by lazy {
|
||||
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDataBaseTables() {
|
||||
// mention db object to connect
|
||||
DBMangaer.db
|
||||
// val db = DBMangaer.db
|
||||
// db.useNestedTransactions = true
|
||||
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
ExtensionTable,
|
||||
SourceTable,
|
||||
MangaTable,
|
||||
ChapterTable,
|
||||
PageTable,
|
||||
CategoryTable,
|
||||
CategoryMangaTable,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class ChapterDataClass(
|
||||
val id: Int,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val date_upload: Long,
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
val pageCount: Int? = null,
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class SourceDataClass(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val iconUrl: String,
|
||||
val supportsLatest: Boolean
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* 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 ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
|
||||
|
||||
var name by ExtensionTable.name
|
||||
var pkgName by ExtensionTable.pkgName
|
||||
var versionName by ExtensionTable.versionName
|
||||
var versionCode by ExtensionTable.versionCode
|
||||
var lang by ExtensionTable.lang
|
||||
var isNsfw by ExtensionTable.isNsfw
|
||||
var apkName by ExtensionTable.apkName
|
||||
var iconUrl by ExtensionTable.iconUrl
|
||||
var installed by ExtensionTable.installed
|
||||
var classFQName by ExtensionTable.classFQName
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* 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 ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<MangaEntity>(MangaTable)
|
||||
|
||||
var url by MangaTable.url
|
||||
var title by MangaTable.title
|
||||
var initialized by MangaTable.initialized
|
||||
|
||||
var artist by MangaTable.artist
|
||||
var author by MangaTable.author
|
||||
var description by MangaTable.description
|
||||
var genre by MangaTable.genre
|
||||
var status by MangaTable.status
|
||||
var thumbnail_url by MangaTable.thumbnail_url
|
||||
|
||||
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* 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 ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.LongEntity
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||
companion object : EntityClass<Long, SourceEntity>(SourceTable, null)
|
||||
|
||||
var sourceId by SourceTable.id
|
||||
var name by SourceTable.name
|
||||
var lang by SourceTable.lang
|
||||
var extension by ExtensionEntity referencedOn SourceTable.extension
|
||||
var partOfFactorySource by SourceTable.partOfFactorySource
|
||||
var positionInFactorySource by SourceTable.positionInFactorySource
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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 org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ChapterTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
val name = varchar("name", 512)
|
||||
val date_upload = long("date_upload").default(0)
|
||||
val chapter_number = float("chapter_number").default(-1f)
|
||||
val scanlator = varchar("scanlator", 128).nullable()
|
||||
|
||||
val manga = reference("manga", MangaTable)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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 org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ExtensionTable : IntIdTable() {
|
||||
val name = varchar("name", 128)
|
||||
val pkgName = varchar("pkg_name", 128)
|
||||
val versionName = varchar("version_name", 16)
|
||||
val versionCode = integer("version_code")
|
||||
val lang = varchar("lang", 10)
|
||||
val isNsfw = bool("is_nsfw")
|
||||
val apkName = varchar("apk_name", 1024)
|
||||
val iconUrl = varchar("icon_url", 2048)
|
||||
|
||||
val installed = bool("installed").default(false)
|
||||
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* 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 ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
object Category {
|
||||
/**
|
||||
* The new category will be placed at the end of the list
|
||||
*/
|
||||
fun createCategory(name: String) {
|
||||
transaction {
|
||||
val count = CategoryTable.selectAll().count()
|
||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||
CategoryTable.insert {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.order] = count.toInt() + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
|
||||
transaction {
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
if (name != null) it[CategoryTable.name] = name
|
||||
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the category from position `from` to `to`
|
||||
*/
|
||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||
transaction {
|
||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
categories.add(to - 1, categories.removeAt(from - 1))
|
||||
categories.forEachIndexed { index, cat ->
|
||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||
it[CategoryTable.order] = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCategory(categoryId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
||||
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
||||
}
|
||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryList(): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* 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 ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
object CategoryManga {
|
||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||
transaction {
|
||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
||||
CategoryMangaTable.insert {
|
||||
it[CategoryMangaTable.category] = categoryId
|
||||
it[CategoryMangaTable.manga] = mangaId
|
||||
}
|
||||
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* list of mangas that belong to a category
|
||||
*/
|
||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* list of categories that a manga belongs to
|
||||
*/
|
||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import ir.armor.tachidesk.impl.Manga.getManga
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import ir.armor.tachidesk.model.database.table.PageTable
|
||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
object Chapter {
|
||||
/** get chapter list when showing a manga */
|
||||
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> {
|
||||
return if (onlineFetch == true) {
|
||||
getSourceChapters(mangaId)
|
||||
} else {
|
||||
transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
|
||||
.map {
|
||||
ChapterTable.toDataClass(it)
|
||||
}
|
||||
}.ifEmpty {
|
||||
// If it was explicitly set to offline dont grab chapters
|
||||
if (onlineFetch == null) {
|
||||
getSourceChapters(mangaId)
|
||||
} else emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
||||
val mangaDetails = getManga(mangaId)
|
||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||
val chapterList = source.fetchChapterList(
|
||||
SManga.create().apply {
|
||||
title = mangaDetails.title
|
||||
url = mangaDetails.url
|
||||
}
|
||||
).awaitSingle()
|
||||
|
||||
val chapterCount = chapterList.count()
|
||||
|
||||
transaction {
|
||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||
if (chapterEntry == null) {
|
||||
ChapterTable.insert {
|
||||
it[url] = fetchedChapter.url
|
||||
it[name] = fetchedChapter.name
|
||||
it[date_upload] = fetchedChapter.date_upload
|
||||
it[chapter_number] = fetchedChapter.chapter_number
|
||||
it[scanlator] = fetchedChapter.scanlator
|
||||
|
||||
it[chapterIndex] = index + 1
|
||||
it[manga] = mangaId
|
||||
}
|
||||
} else {
|
||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||
it[name] = fetchedChapter.name
|
||||
it[date_upload] = fetchedChapter.date_upload
|
||||
it[chapter_number] = fetchedChapter.chapter_number
|
||||
it[scanlator] = fetchedChapter.scanlator
|
||||
|
||||
it[chapterIndex] = index + 1
|
||||
it[manga] = mangaId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear any orphaned chapters that are in the db but not in `chapterList`
|
||||
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
|
||||
|
||||
dbChapterList.forEach {
|
||||
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
||||
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
|
||||
) {
|
||||
transaction {
|
||||
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
|
||||
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dbChapterMap = transaction {
|
||||
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||
.associateBy({ it[ChapterTable.url] }, { it })
|
||||
}
|
||||
|
||||
return chapterList.mapIndexed { index, it ->
|
||||
|
||||
val dbChapter = dbChapterMap.getValue(it.url)
|
||||
|
||||
ChapterDataClass(
|
||||
it.url,
|
||||
it.name,
|
||||
it.date_upload,
|
||||
it.chapter_number,
|
||||
it.scanlator,
|
||||
mangaId,
|
||||
|
||||
dbChapter[ChapterTable.isRead],
|
||||
dbChapter[ChapterTable.isBookmarked],
|
||||
dbChapter[ChapterTable.lastPageRead],
|
||||
|
||||
chapterCount - index,
|
||||
chapterList.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** used to display a chapter, get a chapter in order to show it's pages */
|
||||
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||
val chapterEntry = transaction {
|
||||
ChapterTable.select {
|
||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||
}.first()
|
||||
}
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val pageList = source.fetchPageList(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
}
|
||||
).awaitSingle()
|
||||
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||
|
||||
// update page list for this chapter
|
||||
transaction {
|
||||
pageList.forEach { page ->
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||
if (pageEntry == null) {
|
||||
PageTable.insert {
|
||||
it[index] = page.index
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
it[chapter] = chapterId
|
||||
}
|
||||
} else {
|
||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ChapterDataClass(
|
||||
chapterEntry[ChapterTable.url],
|
||||
chapterEntry[ChapterTable.name],
|
||||
chapterEntry[ChapterTable.date_upload],
|
||||
chapterEntry[ChapterTable.chapter_number],
|
||||
chapterEntry[ChapterTable.scanlator],
|
||||
mangaId,
|
||||
chapterEntry[ChapterTable.isRead],
|
||||
chapterEntry[ChapterTable.isBookmarked],
|
||||
chapterEntry[ChapterTable.lastPageRead],
|
||||
|
||||
chapterEntry[ChapterTable.chapterIndex],
|
||||
chapterCount.toInt(),
|
||||
pageList.count()
|
||||
)
|
||||
}
|
||||
|
||||
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||
transaction {
|
||||
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update ->
|
||||
isRead?.also {
|
||||
update[ChapterTable.isRead] = it
|
||||
}
|
||||
isBookmarked?.also {
|
||||
update[ChapterTable.isBookmarked] = it
|
||||
}
|
||||
lastPageRead?.also {
|
||||
update[ChapterTable.lastPageRead] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markPrevRead?.let {
|
||||
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) {
|
||||
it[ChapterTable.isRead] = markPrevRead
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* 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 ir.armor.tachidesk.impl.Manga.getManga
|
||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
object Library {
|
||||
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
||||
// ..implement that shit at some time...
|
||||
// ..also Consider to rename it to `isDefault`
|
||||
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
transaction {
|
||||
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
|
||||
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[MangaTable.inLibrary] = true
|
||||
it[MangaTable.defaultCategory] = defaultCategories.isEmpty()
|
||||
}
|
||||
|
||||
defaultCategories.forEach { category ->
|
||||
CategoryMangaTable.insert {
|
||||
it[CategoryMangaTable.category] = category[CategoryTable.id].value
|
||||
it[CategoryMangaTable.manga] = mangaId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeMangaFromLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (manga.inLibrary) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = false
|
||||
it[defaultCategory] = true
|
||||
}
|
||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryMangas(): List<MangaDataClass> {
|
||||
return transaction {
|
||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||
import ir.armor.tachidesk.impl.Source.getSource
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.impl.util.await
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.server.ApplicationDirs
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import java.io.InputStream
|
||||
|
||||
object Manga {
|
||||
private fun truncate(text: String?, maxLength: Int): String? {
|
||||
return if (text?.length ?: 0 > maxLength)
|
||||
text?.take(maxLength - 3) + "..."
|
||||
else
|
||||
text
|
||||
}
|
||||
|
||||
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
|
||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary],
|
||||
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||
false
|
||||
)
|
||||
} else { // initialize manga
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val fetchedManga = source.fetchMangaDetails(
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
}
|
||||
).awaitSingle()
|
||||
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
|
||||
it[MangaTable.initialized] = true
|
||||
|
||||
it[MangaTable.artist] = fetchedManga.artist
|
||||
it[MangaTable.author] = fetchedManga.author
|
||||
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
||||
it[MangaTable.genre] = fetchedManga.genre
|
||||
it[MangaTable.status] = fetchedManga.status
|
||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||
}
|
||||
}
|
||||
|
||||
clearMangaThumbnail(mangaId)
|
||||
|
||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
true,
|
||||
|
||||
fetchedManga.artist,
|
||||
fetchedManga.author,
|
||||
fetchedManga.description,
|
||||
fetchedManga.genre,
|
||||
MangaStatus.valueOf(fetchedManga.status).name,
|
||||
false,
|
||||
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
return getCachedImageResponse(saveDir, fileName) {
|
||||
getManga(mangaId) // make sure is initialized
|
||||
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
val source = getHttpSource(sourceId)
|
||||
|
||||
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
|
||||
|
||||
source.client.newCall(
|
||||
GET(thumbnailUrl, source.headers)
|
||||
).await()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearMangaThumbnail(mangaId: Int) {
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
clearCachedImage(saveDir, fileName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object MangaList {
|
||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||
return "/api/v1/manga/$mangaId/thumbnail"
|
||||
}
|
||||
|
||||
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId)
|
||||
val mangasPage = if (popular) {
|
||||
source.fetchPopularManga(pageNum).awaitSingle()
|
||||
} else {
|
||||
if (source.supportsLatest)
|
||||
source.fetchLatestUpdates(pageNum).awaitSingle()
|
||||
else
|
||||
throw Exception("Source $source doesn't support latest")
|
||||
}
|
||||
return mangasPage.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
val mangasPage = this
|
||||
val mangaList = transaction {
|
||||
return@transaction mangasPage.mangas.map { manga ->
|
||||
val mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
val mangaId = MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
it[artist] = manga.artist
|
||||
it[author] = manga.author
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
|
||||
it[sourceReference] = sourceId
|
||||
}.value
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId.toString(),
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
manga.initialized,
|
||||
|
||||
manga.artist,
|
||||
manga.author,
|
||||
manga.description,
|
||||
manga.genre,
|
||||
MangaStatus.valueOf(manga.status).name
|
||||
)
|
||||
} else {
|
||||
val mangaId = mangaEntry[MangaTable.id].value
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId.toString(),
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return PagedMangaListDataClass(
|
||||
mangaList,
|
||||
mangasPage.hasNextPage
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||
import ir.armor.tachidesk.impl.util.storage.SafePath
|
||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import ir.armor.tachidesk.model.database.table.PageTable
|
||||
import ir.armor.tachidesk.server.ApplicationDirs
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Page {
|
||||
/**
|
||||
* A page might have a imageUrl ready from the get go, or we might need to
|
||||
* go an extra step and call fetchImageUrl to get it.
|
||||
*/
|
||||
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
if (page.imageUrl == null) {
|
||||
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
||||
}
|
||||
return page.imageUrl!!
|
||||
}
|
||||
|
||||
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val chapterEntry = transaction {
|
||||
ChapterTable.select {
|
||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||
}.first()
|
||||
}
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.first() }
|
||||
|
||||
val tachiPage = Page(
|
||||
pageEntry[PageTable.index],
|
||||
pageEntry[PageTable.url],
|
||||
pageEntry[PageTable.imageUrl]
|
||||
)
|
||||
|
||||
if (pageEntry[PageTable.imageUrl] == null) {
|
||||
val trueImageUrl = getTrueImageUrl(tachiPage, source)
|
||||
transaction {
|
||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||
it[imageUrl] = trueImageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val saveDir = getChapterDir(mangaId, chapterId)
|
||||
File(saveDir).mkdirs()
|
||||
val fileName = String.format("%03d", index) // e.g. 001.jpeg
|
||||
|
||||
return getCachedImageResponse(saveDir, fileName) {
|
||||
source.fetchImage(tachiPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
private fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
||||
|
||||
val sourceDir = source.toString()
|
||||
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
||||
val chapterDir = SafePath.buildValidFilename(
|
||||
when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
)
|
||||
|
||||
return "${applicationDirs.mangaRoot}/$sourceDir/$mangaDir/$chapterDir"
|
||||
}
|
||||
}
|
||||
+31
-18
@@ -1,30 +1,42 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* 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 ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
|
||||
import ir.armor.tachidesk.impl.MangaList.processEntries
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||
|
||||
fun sourceFilters(sourceId: Long) {
|
||||
val source = getHttpSource(sourceId)
|
||||
// source.getFilterList().toItems()
|
||||
}
|
||||
object Search {
|
||||
// TODO
|
||||
fun sourceFilters(sourceId: Long) {
|
||||
val source = getHttpSource(sourceId)
|
||||
// source.getFilterList().toItems()
|
||||
}
|
||||
|
||||
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId)
|
||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId)
|
||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun sourceGlobalSearch(searchTerm: String) {
|
||||
}
|
||||
fun sourceGlobalSearch(searchTerm: String) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
data class FilterWrapper(
|
||||
val type: String,
|
||||
val filter: Any
|
||||
)
|
||||
data class FilterWrapper(
|
||||
val type: String,
|
||||
val filter: Any
|
||||
)
|
||||
|
||||
/**
|
||||
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
|
||||
*/
|
||||
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
|
||||
// return mapNotNull { filter ->
|
||||
// when (filter) {
|
||||
@@ -60,3 +72,4 @@ data class FilterWrapper(
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* 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 ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||
import ir.armor.tachidesk.model.dataclass.SourceDataClass
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object Source {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun getSourceList(): List<SourceDataClass> {
|
||||
return transaction {
|
||||
SourceTable.selectAll().map {
|
||||
SourceDataClass(
|
||||
it[SourceTable.id].value.toString(),
|
||||
it[SourceTable.name],
|
||||
it[SourceTable.lang],
|
||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSource(sourceId: Long): SourceDataClass {
|
||||
return transaction {
|
||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||
|
||||
SourceDataClass(
|
||||
sourceId.toString(),
|
||||
source?.get(SourceTable.name),
|
||||
source?.get(SourceTable.lang),
|
||||
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
||||
source?.let { getHttpSource(sourceId).supportsLatest }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ir.armor.tachidesk.impl.backup
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class BackupFlags(
|
||||
val includeManga: Boolean,
|
||||
val includeCategories: Boolean,
|
||||
val includeChapters: Boolean,
|
||||
val includeTracking: Boolean,
|
||||
val includeHistory: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.CategoryTypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.ChapterTypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.HistoryTypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.MangaTypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.TrackTypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
||||
import java.util.Date
|
||||
|
||||
open class LegacyBackupBase {
|
||||
protected val parser: Gson = when (version) {
|
||||
2 -> GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
.create()
|
||||
else -> throw Exception("Unknown backup version")
|
||||
}
|
||||
|
||||
protected var sourceMapping: Map<Long, String> = emptyMap()
|
||||
|
||||
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
companion object {
|
||||
internal const val version = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.github.salomonbrys.kotson.set
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object LegacyBackupExport : LegacyBackupBase() {
|
||||
|
||||
suspend fun createLegacyBackup(flags: BackupFlags): String? {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
// Create manga array
|
||||
val mangaEntries = JsonArray()
|
||||
|
||||
// Create category array
|
||||
val categoryEntries = JsonArray()
|
||||
|
||||
// Create extension ID/name mapping
|
||||
val extensionEntries = JsonArray()
|
||||
|
||||
// Add values to root
|
||||
root[Backup.VERSION] = CURRENT_VERSION
|
||||
root[Backup.MANGAS] = mangaEntries
|
||||
root[Backup.CATEGORIES] = categoryEntries
|
||||
root[Backup.EXTENSIONS] = extensionEntries
|
||||
|
||||
transaction {
|
||||
val mangas = MangaTable.select { (MangaTable.inLibrary eq true) }
|
||||
|
||||
val extensions: MutableSet<String> = mutableSetOf()
|
||||
|
||||
// Backup library manga and its dependencies
|
||||
mangas.map {
|
||||
MangaImpl.fromQuery(it)
|
||||
}.forEach { manga ->
|
||||
|
||||
mangaEntries.add(backupMangaObject(manga, flags))
|
||||
|
||||
// Maintain set of extensions/sources used (excludes local source)
|
||||
if (manga.source != LocalSource.ID) {
|
||||
getHttpSource(manga.source).let {
|
||||
extensions.add("${it.id}:${it.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup categories
|
||||
if (flags.includeCategories) {
|
||||
backupCategories(categoryEntries)
|
||||
}
|
||||
|
||||
// Backup extension ID/name mapping
|
||||
backupExtensionInfo(extensionEntries, extensions)
|
||||
}
|
||||
|
||||
return parser.toJson(root)
|
||||
}
|
||||
|
||||
private fun backupMangaObject(manga: Manga, options: BackupFlags): JsonElement {
|
||||
// Entry for this manga
|
||||
val entry = JsonObject()
|
||||
|
||||
// Backup manga fields
|
||||
entry[Backup.MANGA] = parser.toJsonTree(manga)
|
||||
val mangaId = manga.id!!.toInt()
|
||||
|
||||
// Check if user wants chapter information in backup
|
||||
if (options.includeChapters) {
|
||||
// Backup all the chapters
|
||||
val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { ChapterImpl.fromQuery(it) }
|
||||
if (chapters.count() > 0) {
|
||||
val chaptersJson = parser.toJsonTree(chapters)
|
||||
if (chaptersJson.asJsonArray.size() > 0) {
|
||||
entry[Backup.CHAPTERS] = chaptersJson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options.includeCategories) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = getMangaCategories(mangaId)
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
val categoriesNames = categoriesForManga.map { it.name }
|
||||
entry[Backup.CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options.includeTracking) { // TODO
|
||||
// val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
// if (tracks.isNotEmpty()) {
|
||||
// entry[TRACK] = parser.toJsonTree(tracks)
|
||||
// }
|
||||
}
|
||||
//
|
||||
// // Check if user wants history information in backup
|
||||
if (options.includeHistory) { // TODO
|
||||
// val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||
// if (historyForManga.isNotEmpty()) {
|
||||
// val historyData = historyForManga.mapNotNull { history ->
|
||||
// val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||
// url?.let { DHistory(url, history.last_read) }
|
||||
// }
|
||||
// val historyJson = parser.toJsonTree(historyData)
|
||||
// if (historyJson.asJsonArray.size() > 0) {
|
||||
// entry[HISTORY] = historyJson
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
private fun backupCategories(root: JsonArray) {
|
||||
val categories = getCategoryList().map {
|
||||
CategoryImpl().apply {
|
||||
name = it.name
|
||||
order = it.order
|
||||
}
|
||||
}
|
||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||
}
|
||||
|
||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||
extensions.sorted().forEach {
|
||||
root.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy
|
||||
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import ir.armor.tachidesk.impl.Category.createCategory
|
||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.ValidationResult
|
||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.validate
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.Chapter
|
||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
||||
import ir.armor.tachidesk.impl.backup.models.Track
|
||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import java.io.InputStream
|
||||
import java.util.Date
|
||||
|
||||
/*
|
||||
* 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/. */
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
object LegacyBackupImport : LegacyBackupBase() {
|
||||
suspend fun restoreLegacyBackup(sourceStream: InputStream): ValidationResult {
|
||||
val reader = sourceStream.bufferedReader()
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
|
||||
val validationResult = validate(json)
|
||||
|
||||
val mangasJson = json.get(Backup.MANGAS).asJsonArray
|
||||
|
||||
// Restore categories
|
||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = LegacyBackupValidator.getSourceMapping(json)
|
||||
|
||||
// Restore individual manga
|
||||
mangasJson.forEach {
|
||||
restoreManga(it.asJsonObject)
|
||||
}
|
||||
|
||||
logger.info {
|
||||
"""
|
||||
Restore Errors:
|
||||
${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
|
||||
Restore Summary:
|
||||
- Missing Sources:
|
||||
${validationResult.missingSources.joinToString("\n")}
|
||||
- Missing Trackers:
|
||||
${validationResult.missingTrackers.joinToString("\n")}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
return validationResult
|
||||
}
|
||||
|
||||
private fun restoreCategories(jsonCategories: JsonElement) {
|
||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||
val dbCategories = getCategoryList()
|
||||
|
||||
// Iterate over them and create missing categories
|
||||
backupCategories.forEach { category ->
|
||||
if (dbCategories.none { it.name == category.name }) {
|
||||
createCategory(category.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||
val manga = parser.fromJson<MangaImpl>(
|
||||
mangaJson.get(
|
||||
Backup.MANGA
|
||||
)
|
||||
)
|
||||
val chapters = parser.fromJson<List<ChapterImpl>>(
|
||||
mangaJson.get(Backup.CHAPTERS)
|
||||
?: JsonArray()
|
||||
)
|
||||
val categories = parser.fromJson<List<String>>(
|
||||
mangaJson.get(Backup.CATEGORIES)
|
||||
?: JsonArray()
|
||||
)
|
||||
val history = parser.fromJson<List<DHistory>>(
|
||||
mangaJson.get(Backup.HISTORY)
|
||||
?: JsonArray()
|
||||
)
|
||||
val tracks = parser.fromJson<List<TrackImpl>>(
|
||||
mangaJson.get(Backup.TRACK)
|
||||
?: JsonArray()
|
||||
)
|
||||
|
||||
val source = try {
|
||||
getHttpSource(manga.source)
|
||||
} catch (e: NullPointerException) {
|
||||
null
|
||||
} catch (e: NoSuchElementException) {
|
||||
null
|
||||
}
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
||||
logger.debug("Restoring Manga: ${manga.title} from $sourceName")
|
||||
|
||||
try {
|
||||
if (source != null) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: Source not found: $sourceName (${manga.source})")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param manga manga data from json
|
||||
* @param source source to get manga data from
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private suspend fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
val fetchedManga = fetchManga(source, manga)
|
||||
|
||||
updateChapters(source, fetchedManga, chapters)
|
||||
|
||||
// TODO
|
||||
// backupManager.restoreCategoriesForManga(manga, categories)
|
||||
|
||||
// backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// backupManager.restoreTrackForManga(manga, tracks)
|
||||
|
||||
// updateTracking(fetchedManga, tracks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga.
|
||||
*/
|
||||
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
|
||||
// make sure we have the manga record in library
|
||||
transaction {
|
||||
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
|
||||
MangaTable.insert {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
it[sourceReference] = manga.source
|
||||
}
|
||||
}
|
||||
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||
it[MangaTable.inLibrary] = true
|
||||
}
|
||||
}
|
||||
|
||||
// update manga details
|
||||
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
|
||||
transaction {
|
||||
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||
|
||||
it[artist] = fetchedManga.artist
|
||||
it[author] = fetchedManga.author
|
||||
it[description] = fetchedManga.description
|
||||
it[genre] = fetchedManga.genre
|
||||
it[status] = fetchedManga.status
|
||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||
}
|
||||
}
|
||||
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy
|
||||
|
||||
/*
|
||||
* 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.google.gson.JsonObject
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object LegacyBackupValidator {
|
||||
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
*
|
||||
* @throws Exception if version or manga cannot be found.
|
||||
* @return List of missing sources or missing trackers.
|
||||
*/
|
||||
fun validate(json: JsonObject): ValidationResult {
|
||||
val version = json.get(Backup.VERSION)
|
||||
val mangasJson = json.get(Backup.MANGAS)
|
||||
if (version == null || mangasJson == null) {
|
||||
throw Exception("File is missing data.")
|
||||
}
|
||||
|
||||
val mangas = mangasJson.asJsonArray
|
||||
if (mangas.size() == 0) {
|
||||
throw Exception("Backup does not contain any manga.")
|
||||
}
|
||||
|
||||
val sources = getSourceMapping(json)
|
||||
val missingSources = transaction {
|
||||
sources
|
||||
.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
||||
.map { "${it.value} (${it.key})" }
|
||||
.sorted()
|
||||
}
|
||||
|
||||
val trackers = mangas
|
||||
.filter { it.asJsonObject.has("track") }
|
||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
.map { it.asJsonObject["s"].asInt }
|
||||
.distinct()
|
||||
|
||||
val missingTrackers = listOf("")
|
||||
// val missingTrackers = trackers
|
||||
// .mapNotNull { trackManager.getService(it) }
|
||||
// .filter { !it.isLogged }
|
||||
// .map { context.getString(it.nameRes()) }
|
||||
// .sorted()
|
||||
|
||||
return ValidationResult(missingSources, missingTrackers)
|
||||
}
|
||||
|
||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||
|
||||
return extensionsMapping.asJsonArray
|
||||
.map {
|
||||
val items = it.asString.split(":")
|
||||
items[0].toLong() to items[1]
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.models
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Json values
|
||||
*/
|
||||
object Backup {
|
||||
const val CURRENT_VERSION = 2
|
||||
const val MANGA = "manga"
|
||||
const val MANGAS = "mangas"
|
||||
const val TRACK = "track"
|
||||
const val CHAPTERS = "chapters"
|
||||
const val CATEGORIES = "categories"
|
||||
const val EXTENSIONS = "extensions"
|
||||
const val HISTORY = "history"
|
||||
const val VERSION = "version"
|
||||
|
||||
fun getDefaultFilename(): String {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||
return "tachiyomi_$date.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.models
|
||||
|
||||
data class DHistory(val url: String, val lastRead: Long)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||
*/
|
||||
object CategoryTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<CategoryImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.name)
|
||||
value(it.order)
|
||||
endArray()
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
val category = CategoryImpl()
|
||||
category.name = nextString()
|
||||
category.order = nextInt()
|
||||
endArray()
|
||||
category
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonToken
|
||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||
*/
|
||||
object ChapterTypeAdapter {
|
||||
|
||||
private const val URL = "u"
|
||||
private const val READ = "r"
|
||||
private const val BOOKMARK = "b"
|
||||
private const val LAST_READ = "l"
|
||||
|
||||
fun build(): TypeAdapter<ChapterImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||
beginObject()
|
||||
name(URL)
|
||||
value(it.url)
|
||||
if (it.read) {
|
||||
name(READ)
|
||||
value(1)
|
||||
}
|
||||
if (it.bookmark) {
|
||||
name(BOOKMARK)
|
||||
value(1)
|
||||
}
|
||||
if (it.last_page_read != 0) {
|
||||
name(LAST_READ)
|
||||
value(it.last_page_read)
|
||||
}
|
||||
endObject()
|
||||
}
|
||||
}
|
||||
|
||||
read {
|
||||
val chapter = ChapterImpl()
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
if (peek() == JsonToken.NAME) {
|
||||
when (nextName()) {
|
||||
URL -> chapter.url = nextString()
|
||||
READ -> chapter.read = nextInt() == 1
|
||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||
LAST_READ -> chapter.last_page_read = nextInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
endObject()
|
||||
chapter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [DHistory] to / from json
|
||||
*/
|
||||
object HistoryTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<DHistory> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
if (it.lastRead != 0L) {
|
||||
beginArray()
|
||||
value(it.url)
|
||||
value(it.lastRead)
|
||||
endArray()
|
||||
}
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
val url = nextString()
|
||||
val lastRead = nextLong()
|
||||
endArray()
|
||||
DHistory(url, lastRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||
*/
|
||||
object MangaTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<MangaImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.url)
|
||||
value(it.title)
|
||||
value(it.source)
|
||||
value(it.viewer)
|
||||
value(it.chapter_flags)
|
||||
endArray()
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
val manga = MangaImpl()
|
||||
manga.url = nextString()
|
||||
manga.title = nextString()
|
||||
manga.source = nextLong()
|
||||
manga.viewer = nextInt()
|
||||
manga.chapter_flags = nextInt()
|
||||
endArray()
|
||||
manga
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonToken
|
||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||
*/
|
||||
object TrackTypeAdapter {
|
||||
|
||||
private const val SYNC = "s"
|
||||
private const val MEDIA = "r"
|
||||
private const val LIBRARY = "ml"
|
||||
private const val TITLE = "t"
|
||||
private const val LAST_READ = "l"
|
||||
private const val TRACKING_URL = "u"
|
||||
|
||||
fun build(): TypeAdapter<TrackImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginObject()
|
||||
name(TITLE)
|
||||
value(it.title)
|
||||
name(SYNC)
|
||||
value(it.sync_id)
|
||||
name(MEDIA)
|
||||
value(it.media_id)
|
||||
name(LIBRARY)
|
||||
value(it.library_id)
|
||||
name(LAST_READ)
|
||||
value(it.last_chapter_read)
|
||||
name(TRACKING_URL)
|
||||
value(it.tracking_url)
|
||||
endObject()
|
||||
}
|
||||
|
||||
read {
|
||||
val track = TrackImpl()
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
if (peek() == JsonToken.NAME) {
|
||||
when (nextName()) {
|
||||
TITLE -> track.title = nextString()
|
||||
SYNC -> track.sync_id = nextInt()
|
||||
MEDIA -> track.media_id = nextInt()
|
||||
LIBRARY -> track.library_id = nextLong()
|
||||
LAST_READ -> track.last_chapter_read = nextInt()
|
||||
TRACKING_URL -> track.tracking_url = nextString()
|
||||
}
|
||||
}
|
||||
}
|
||||
endObject()
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ir.armor.tachidesk.impl.backup.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Category : Serializable {
|
||||
|
||||
var id: Int?
|
||||
|
||||
var name: String
|
||||
|
||||
var order: Int
|
||||
|
||||
var flags: Int
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ir.armor.tachidesk.impl.backup.models
|
||||
|
||||
class CategoryImpl : Category {
|
||||
|
||||
override var id: Int? = null
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var order: Int = 0
|
||||
|
||||
override var flags: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val category = other as Category
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user