Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
706fa82a37 | |||
57c54fa275 | |||
38c5234e46 | |||
767ee164e6 |
32
.github/CONTRIBUTING.md
vendored
@ -1,27 +1,20 @@
|
|||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
|
||||||
3. What is your type of issue?
|
|
||||||
* [Catalogue request](#catalogue-requests)
|
|
||||||
* [Bugs](#bugs)
|
|
||||||
* [Feature requests](#feature-requests)
|
|
||||||
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
|
||||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
# Catalogue requests
|
|
||||||
|
|
||||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
|
||||||
|
|
||||||
# Bugs
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (Setting > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Dev version is equal to the number of commits as seen in the main page
|
* Dev version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
||||||
|
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* Don't group unrelated requests into one issue
|
* For multipart issues **use list** like this:
|
||||||
|
* [x] Done
|
||||||
|
* [ ] Not done
|
||||||
|
```
|
||||||
|
* [x] Done
|
||||||
|
* [ ] Not done
|
||||||
|
```
|
||||||
|
* Don't put together too many unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
@ -31,3 +24,8 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|||||||
|
|
||||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
||||||
|
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
||||||
|
16
.github/ISSUE_TEMPLATE.md
vendored
@ -1,15 +1,7 @@
|
|||||||
**DO NOT OPEN ISSUES/REQUESTS RELATING TO EXTENSIONS/CATALOGUES IN THIS REPOSITORY. Open them at the following repository https://github.com/inorichi/tachiyomi-extensions/**
|
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
||||||
|
|
||||||
**For all other requests Please fill out the form below and remove the first 3 lines of this template**
|
Remove line above and describe your issue here. Fill out version below. Use Preview.
|
||||||
|
|
||||||
**App version:**
|
|
||||||
|
|
||||||
**Issue/Request:**
|
Version: r000 or v0.0.0
|
||||||
|
(other relevant info like OS)
|
||||||
**Steps to reproduce (if applicable)**
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
**Other details:**
|
|
||||||
|
BIN
.github/readme-images/app-icon.png
vendored
Before Width: | Height: | Size: 1.1 KiB |
BIN
.github/readme-images/screens.png
vendored
Before Width: | Height: | Size: 730 KiB |
72
.travis.yml
@ -1,58 +1,28 @@
|
|||||||
language: android
|
language: android
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- build-tools-28.0.3
|
- platform-tools
|
||||||
- android-27
|
- tools
|
||||||
- extra-android-m2repository
|
|
||||||
- extra-google-m2repository
|
# The BuildTools version used by your project
|
||||||
- extra-android-support
|
- build-tools-23.0.3
|
||||||
- extra-google-google_play_services
|
- android-23
|
||||||
licenses:
|
- extra-android-m2repository
|
||||||
- android-sdk-license-.+
|
- extra-google-m2repository
|
||||||
before_install:
|
- extra-android-support
|
||||||
- yes | sdkmanager "platforms;android-27" # workaround for accepting the license
|
- extra-google-google_play_services
|
||||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
|
||||||
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
before_script:
|
||||||
tar xf secrets.tar;
|
- chmod +x gradlew
|
||||||
mv debug.keystore "$HOME/.android";
|
#Build, and run tests
|
||||||
fi
|
script: "./gradlew clean buildDebug"
|
||||||
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root
|
sudo: false
|
||||||
- export ANDROID_NDK_HOME=$HOME/android-ndk-root
|
|
||||||
- mkdir "$ANDROID_HOME/licenses" || true
|
|
||||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
|
||||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
|
||||||
script: ".travis/build.sh"
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- "$HOME/.gradle/caches/"
|
- $HOME/.gradle/caches/
|
||||||
- "$HOME/.gradle/wrapper/"
|
- $HOME/.gradle/wrapper/
|
||||||
- "$HOME/.android/build-cache"
|
|
||||||
deploy:
|
|
||||||
- provider: releases
|
|
||||||
api_key:
|
|
||||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
|
||||||
file: tachiyomi-v*.apk
|
|
||||||
file_glob: true
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
repo: inorichi/tachiyomi
|
|
||||||
- provider: script
|
|
||||||
script: ".travis/deploy.sh"
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
condition: "-z $TRAVIS_TAG"
|
|
||||||
repo: inorichi/tachiyomi
|
|
||||||
env:
|
env:
|
||||||
global:
|
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
||||||
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
|
||||||
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
|
||||||
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
|
||||||
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
|
||||||
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
|
||||||
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
|
||||||
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
git fetch --unshallow #required for commit count
|
|
||||||
|
|
||||||
cp .travis/google-services.json app/
|
|
||||||
|
|
||||||
if [ -z "$TRAVIS_TAG" ]; then
|
|
||||||
./gradlew clean assembleStandardDebug
|
|
||||||
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
|
|
||||||
else
|
|
||||||
./gradlew clean assembleStandardRelease
|
|
||||||
|
|
||||||
TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)"
|
|
||||||
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
|
|
||||||
|
|
||||||
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
|
|
||||||
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
|
|
||||||
fi
|
|
@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
pattern="tachiyomi-r*"
|
|
||||||
files=( $pattern )
|
|
||||||
export ARTIFACT="${files[0]}"
|
|
||||||
|
|
||||||
if [ -z "$ARTIFACT" ]; then
|
|
||||||
echo "Artifact not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
|
|
||||||
|
|
||||||
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
|
|
||||||
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest
|
|
@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "777921915939",
|
|
||||||
"firebase_url": "https://tachiyomi-47364.firebaseio.com",
|
|
||||||
"project_id": "tachiyomi-47364",
|
|
||||||
"storage_bucket": "tachiyomi-47364.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:777921915939:android:36544cd2d96c50c7",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "eu.kanade.tachiyomi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"analytics_service": {
|
|
||||||
"status": 1
|
|
||||||
},
|
|
||||||
"appinvite_service": {
|
|
||||||
"status": 1,
|
|
||||||
"other_platform_oauth_client": []
|
|
||||||
},
|
|
||||||
"ads_service": {
|
|
||||||
"status": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:777921915939:android:564fdc1d62efd1de",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "eu.kanade.tachiyomi.debug"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"analytics_service": {
|
|
||||||
"status": 1
|
|
||||||
},
|
|
||||||
"appinvite_service": {
|
|
||||||
"status": 1,
|
|
||||||
"other_platform_oauth_client": []
|
|
||||||
},
|
|
||||||
"ads_service": {
|
|
||||||
"status": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
74
README.md
@ -1,70 +1,24 @@
|
|||||||
| Build | Stable | Dev | Contribute | Contact |
|
| Build | Download | Auto Update |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|-------------|
|
||||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [)](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) [](//github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
|
||||||
|
|
||||||
|
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
|
||||||
|
|
||||||
# Tachiyomi
|
|
||||||
Tachiyomi is a free and open source manga reader for Android.
|
Tachiyomi is a free and open source manga reader for Android.
|
||||||
|
|
||||||

|
Keep in mind it's still a beta, so expect it to crash sometimes.
|
||||||
|
|
||||||
## Features
|
# Features
|
||||||
|
|
||||||
Features include:
|
* Online and offline reading
|
||||||
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
* Configurable reader with multiple viewers and settings
|
||||||
* Local reading of downloaded manga
|
* MyAnimeList support
|
||||||
* Configurable reader with multiple viewers, reading directions and other settings
|
* Resume from the next unread chapter
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
|
* Chapter filtering
|
||||||
|
* Schedule searching for updates
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
|
||||||
* Schedule updating your library for new chapters
|
|
||||||
* Create backups locally to read offline or to your desired cloud service
|
|
||||||
|
|
||||||
## Download
|
|
||||||
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
|
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest) (auto-updates not included), or add our [F-Droid repo](https://github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions).
|
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
|
||||||
|
|
||||||
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
|
|
||||||
|
|
||||||
<details><summary>Issues</summary>
|
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Bugs</summary>
|
|
||||||
|
|
||||||
* Include version (Setting > About > Version)
|
|
||||||
* If not latest, try updating, it may have already been solved
|
|
||||||
* Dev version is equal to the number of commits as seen in the main page
|
|
||||||
* Include steps to reproduce (if not obvious from description)
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
|
||||||
* Don't group unrelated requests into one issue
|
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
|
||||||
|
|
||||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Feature Requests</summary>
|
|
||||||
|
|
||||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
|
|
||||||
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
|
|
||||||
You can also reach out to us on [Discord](https://discord.gg/WrBkRk4).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
3
app/.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
custom.gradle
|
.idea
|
||||||
google-services.json
|
|
234
app/build.gradle
@ -3,10 +3,6 @@ import java.text.SimpleDateFormat
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
|
||||||
|
|
||||||
shortcutHelper.filePath = './shortcuts.xml'
|
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// Git is needed in your system PATH for these commands to work.
|
// Git is needed in your system PATH for these commands to work.
|
||||||
@ -28,62 +24,43 @@ ext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def includeUpdater() {
|
||||||
|
return hasProperty("include_updater")
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 23
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion "23.0.3"
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 27
|
targetSdkVersion 23
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 38
|
versionCode 9
|
||||||
versionName "0.8.0"
|
versionName "0.2.2-1"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
versionNameSuffix ".${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
multiDexEnabled true
|
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
multiDexEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "default"
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
standard {
|
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
fdroid {
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
dev {
|
|
||||||
minSdkVersion 21
|
|
||||||
resConfigs "en", "xxhdpi"
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude 'META-INF/DEPENDENCIES'
|
exclude 'META-INF/DEPENDENCIES'
|
||||||
exclude 'LICENSE.txt'
|
exclude 'LICENSE.txt'
|
||||||
@ -97,146 +74,123 @@ android {
|
|||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
|
||||||
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
generateStubs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
final SUPPORT_LIBRARY_VERSION = '23.4.0'
|
||||||
|
final DAGGER_VERSION = '2.4'
|
||||||
|
final RETROFIT_VERSION = '2.0.2'
|
||||||
|
final NUCLEUS_VERSION = '3.0.0'
|
||||||
|
final STORIO_VERSION = '1.8.0'
|
||||||
|
final MOCKITO_VERSION = '1.10.19'
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
|
||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '27.0.2'
|
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:support-v4:$support_library_version"
|
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:appcompat-v7:$support_library_version"
|
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:cardview-v7:$support_library_version"
|
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:design:$support_library_version"
|
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:recyclerview-v7:$support_library_version"
|
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:preference-v7:$support_library_version"
|
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:support-annotations:$support_library_version"
|
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
||||||
implementation "com.android.support:customtabs:$support_library_version"
|
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
|
||||||
|
|
||||||
implementation 'com.android.support:multidex:1.0.2'
|
|
||||||
|
|
||||||
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
|
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.0'
|
||||||
implementation 'io.reactivex:rxjava:1.3.6'
|
compile 'io.reactivex:rxjava:1.1.5'
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
||||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
|
||||||
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
|
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
compile "com.squareup.okhttp3:okhttp:3.3.1"
|
||||||
implementation 'com.squareup.okio:okio:1.14.0'
|
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.3.0'
|
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
|
||||||
|
// IO
|
||||||
|
compile 'com.squareup.okio:okio:1.8.0'
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
implementation 'com.google.code.gson:gson:2.8.2'
|
compile 'com.google.code.gson:gson:2.6.2'
|
||||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
|
||||||
|
// YAML
|
||||||
|
compile 'org.yaml:snakeyaml:1.17'
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation 'com.squareup.duktape:duktape-android:1.2.0'
|
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
||||||
|
|
||||||
// Disk
|
// Disk cache
|
||||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
|
||||||
|
|
||||||
// HTML parser
|
// Parse HTML
|
||||||
implementation 'org.jsoup:jsoup:1.10.2'
|
compile 'org.jsoup:jsoup:1.9.2'
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
implementation 'com.evernote:android-job:1.2.5'
|
|
||||||
implementation 'com.google.android.gms:play-services-gcm:11.8.0'
|
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation "com.pushtorefresh.storio:sqlite:1.13.0"
|
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
||||||
|
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
||||||
|
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION"
|
||||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
compile "info.android15.nucleus:nucleus-support-v4:$NUCLEUS_VERSION"
|
||||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
compile "info.android15.nucleus:nucleus-support-v7:$NUCLEUS_VERSION"
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
|
provided 'org.glassfish:javax.annotation:10.0-b28'
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
final glide_version = '4.6.1'
|
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
|
||||||
|
|
||||||
// Transformations
|
|
||||||
implementation 'jp.wasabeef:glide-transformations:3.1.1'
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'com.jakewharton.timber:timber:4.6.1'
|
compile 'com.jakewharton.timber:timber:4.1.2'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
implementation 'ch.acra:acra:4.9.2'
|
compile 'ch.acra:acra:4.8.5'
|
||||||
|
|
||||||
// Sort
|
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
compile 'eu.davidea:flexible-adapter:4.2.0'
|
||||||
implementation 'eu.davidea:flexible-adapter:5.0.0-rc4'
|
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1'
|
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
compile 'com.afollestad.material-dialogs:core:0.8.5.9'
|
||||||
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
|
||||||
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
|
|
||||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
|
||||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
|
||||||
implementation 'com.github.mthli:Slice:v1.2'
|
|
||||||
implementation 'me.gujun.android.taggroup:library:1.4@aar'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
|
||||||
implementation 'com.github.inorichi:DirectionalViewPager:3acc51a'
|
|
||||||
|
|
||||||
// Conductor
|
|
||||||
implementation 'com.bluelinelabs:conductor:2.1.5'
|
|
||||||
implementation ("com.bluelinelabs:conductor-support:2.1.5") {
|
|
||||||
exclude group: "com.android.support"
|
|
||||||
}
|
|
||||||
implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
|
|
||||||
|
|
||||||
// RxBindings
|
|
||||||
final rxbindings_version = '1.0.1'
|
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
|
||||||
|
testCompile('org.robolectric:robolectric:3.0') {
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
||||||
|
}
|
||||||
|
|
||||||
final robolectric_version = '3.1.4'
|
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
|
||||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
||||||
|
|
||||||
final coroutines_version = '0.22.2'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.2.71'
|
ext.kotlin_version = '1.0.2'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -248,17 +202,3 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
experimental {
|
|
||||||
coroutines 'enable'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
}
|
|
||||||
|
111
app/proguard-rules.pro
vendored
@ -1,30 +1,24 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-dontwarn eu.kanade.tachiyomi.**
|
-keep class eu.kanade.tachiyomi.injection.** { *; }
|
||||||
-keep class eu.kanade.tachiyomi.**
|
|
||||||
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
|
||||||
|
|
||||||
-keep class com.hippo.image.** { *; }
|
|
||||||
-keep interface com.hippo.image.** { *; }
|
|
||||||
-dontwarn nucleus.view.NucleusActionBarActivity
|
|
||||||
|
|
||||||
# Extensions may require methods unused in the core app
|
|
||||||
-keep class org.jsoup.** { *; }
|
|
||||||
-keep class kotlin.** { *; }
|
|
||||||
-keep class okhttp3.** { *; }
|
|
||||||
-keep class com.google.gson.** { *; }
|
|
||||||
-keep class com.github.salomonbrys.kotson.** { *; }
|
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
|
-keepattributes Signature
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keep class okhttp3.** { *; }
|
||||||
|
-keep interface okhttp3.** { *; }
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
-dontwarn javax.annotation.**
|
|
||||||
-dontwarn retrofit2.Platform$Java8
|
# Okio
|
||||||
|
-keep class sun.misc.Unsafe { *; }
|
||||||
|
-dontwarn java.nio.file.*
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
|
-dontwarn okio.**
|
||||||
|
|
||||||
# Glide specific rules #
|
# Glide specific rules #
|
||||||
# https://github.com/bumptech/glide
|
# https://github.com/bumptech/glide
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
-keep public class * extends com.bumptech.glide.AppGlideModule
|
|
||||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||||
**[] $VALUES;
|
**[] $VALUES;
|
||||||
public *;
|
public *;
|
||||||
@ -46,46 +40,89 @@
|
|||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
### Support v7, Design
|
# Retrofit 2.X
|
||||||
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
## https://square.github.io/retrofit/ ##
|
||||||
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
|
||||||
|
|
||||||
|
-dontwarn retrofit2.**
|
||||||
|
-keep class retrofit2.** { *; }
|
||||||
|
-keepattributes Signature
|
||||||
|
-keepattributes Exceptions
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@retrofit2.http.* <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# AppCombat
|
||||||
-keep public class android.support.v7.widget.** { *; }
|
-keep public class android.support.v7.widget.** { *; }
|
||||||
-keep public class android.support.v7.internal.widget.** { *; }
|
-keep public class android.support.v7.internal.widget.** { *; }
|
||||||
-keep public class android.support.v7.internal.view.menu.** { *; }
|
-keep public class android.support.v7.internal.view.menu.** { *; }
|
||||||
-keep public class android.support.v7.graphics.drawable.** { *; }
|
|
||||||
|
|
||||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||||
public <init>(android.content.Context);
|
public <init>(android.content.Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
-dontwarn android.support.**
|
## GSON 2.2.4 specific rules ##
|
||||||
-dontwarn android.support.design.**
|
|
||||||
-keep class android.support.design.** { *; }
|
|
||||||
-keep interface android.support.design.** { *; }
|
|
||||||
-keep public class android.support.design.R$* { *; }
|
|
||||||
|
|
||||||
|
|
||||||
# ReactiveNetwork
|
|
||||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
|
||||||
|
|
||||||
## GSON ##
|
|
||||||
|
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||||
# removes such information by default, so configure it to keep all of it.
|
# removes such information by default, so configure it to keep all of it.
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
|
||||||
|
# For using GSON @Expose annotation
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
-keepattributes EnclosingMethod
|
||||||
|
|
||||||
# Gson specific classes
|
# Gson specific classes
|
||||||
-keep class sun.misc.Unsafe { *; }
|
-keep class sun.misc.Unsafe { *; }
|
||||||
|
-keep class com.google.gson.stream.** { *; }
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
## ACRA 4.5.0 specific rules ##
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
# we need line numbers in our stack traces otherwise they are pretty useless
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
-renamesourcefileattribute SourceFile
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# ACRA needs "annotations" so add this...
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'.
|
||||||
|
# Note: if you are removing log messages elsewhere in this file then this isn't necessary
|
||||||
|
-keep class org.acra.ACRA {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
# keep this around for some enums that ACRA needs
|
||||||
|
-keep class org.acra.ReportingInteractionMode {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepnames class org.acra.sender.HttpSender$** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepnames class org.acra.ReportField {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
# keep this otherwise it is removed by ProGuard
|
||||||
|
-keep public class org.acra.ErrorReporter {
|
||||||
|
public void addCustomData(java.lang.String,java.lang.String);
|
||||||
|
public void putCustomData(java.lang.String,java.lang.String);
|
||||||
|
public void removeCustomData(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
# keep this otherwise it is removed by ProGuard
|
||||||
|
-keep public class org.acra.ErrorReporter {
|
||||||
|
public void handleSilentException(java.lang.Throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep the support library
|
||||||
|
-keep class org.acra.** { *; }
|
||||||
|
-keep interface org.acra.** { *; }
|
||||||
|
|
||||||
# SnakeYaml
|
# SnakeYaml
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
-dontwarn org.yaml.snakeyaml.**
|
-dontwarn org.yaml.snakeyaml.**
|
||||||
|
|
||||||
# Duktape
|
# Duktape
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_book_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_library"
|
|
||||||
android:shortcutLongLabel="@string/label_library"
|
|
||||||
android:shortcutShortLabel="@string/label_library">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_update_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_recently_updated"
|
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
|
||||||
android:shortcutShortLabel="@string/short_recent_updates">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_glasses_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_recently_read"
|
|
||||||
android:shortcutLongLabel="@string/label_recent_manga"
|
|
||||||
android:shortcutShortLabel="@string/label_recent_manga">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_explore_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_catalogues"
|
|
||||||
android:shortcutLongLabel="@string/label_catalogues"
|
|
||||||
android:shortcutShortLabel="@string/label_catalogues">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
</shortcuts>
|
|
@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108.0"
|
|
||||||
android:viewportHeight="108.0">
|
|
||||||
<path
|
|
||||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#455A64"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#607D8B"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#CE2828"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#FFF"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@android:color/transparent"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@android:color/transparent"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 18 KiB |
@ -1,93 +1,108 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi" >
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop">
|
android:theme="@style/Theme.BrandedLaunch">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!--suppress AndroidDomInspection -->
|
|
||||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity" />
|
android:name=".ui.manga.MangaActivity"
|
||||||
|
android:parentActivityName=".ui.main.MainActivity" >
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
|
android:theme="@style/Theme.Reader">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.SettingsActivity"
|
||||||
|
android:label="@string/label_settings"
|
||||||
|
android:parentActivityName=".ui.main.MainActivity" >
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.category.CategoryActivity"
|
||||||
|
android:label="@string/label_categories"
|
||||||
|
android:parentActivityName=".ui.main.MainActivity">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme" />
|
android:theme="@style/FilePickerTheme">
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.AnilistLoginActivity"
|
|
||||||
android:label="Anilist">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="anilist-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
|
||||||
|
|
||||||
<provider
|
<service android:name=".data.library.LibraryUpdateService"
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:exported="false"/>
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
<service android:name=".data.download.DownloadService"
|
||||||
android:grantUriPermissions="true">
|
android:exported="false"/>
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||||
android:resource="@xml/provider_paths" />
|
android:exported="false"/>
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
||||||
android:exported="false" />
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name=".data.library.LibraryUpdateService"
|
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
|
||||||
android:exported="false" />
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name=".data.download.DownloadService"
|
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
||||||
android:exported="false" />
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name=".data.updater.UpdaterService"
|
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
|
||||||
android:exported="false" />
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name=".data.backup.BackupCreateService"
|
android:name=".data.library.LibraryUpdateAlarm">
|
||||||
android:exported="false"/>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
<action android:name="eu.kanade.UPDATE_LIBRARY" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<receiver
|
||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.updater.UpdateDownloaderAlarm">
|
||||||
android:exported="false"/>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
<action android:name="eu.kanade.CHECK_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
|
android:value="GlideModule" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
@ -2,73 +2,61 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import android.support.multidex.MultiDex
|
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector
|
||||||
import com.evernote.android.job.JobManager
|
import eu.kanade.tachiyomi.injection.component.AppComponent
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.injection.module.AppModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.InjektScope
|
|
||||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
|
||||||
|
|
||||||
@ReportsCrashes(
|
@ReportsCrashes(
|
||||||
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
||||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||||
buildConfigClass = BuildConfig::class,
|
buildConfigClass = BuildConfig::class,
|
||||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
|
||||||
)
|
)
|
||||||
open class App : Application() {
|
open class App : Application() {
|
||||||
|
|
||||||
|
lateinit var component: AppComponent
|
||||||
|
private set
|
||||||
|
|
||||||
|
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
|
||||||
|
private set
|
||||||
|
|
||||||
|
var appTheme = 0
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
Injekt = InjektScope(DefaultRegistrar())
|
component = prepareAppComponent().build()
|
||||||
Injekt.importModule(AppModule(this))
|
|
||||||
|
|
||||||
|
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
|
||||||
|
|
||||||
|
setupTheme()
|
||||||
setupAcra()
|
setupAcra()
|
||||||
setupJobManager()
|
|
||||||
setupNotificationChannels()
|
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
private fun setupTheme() {
|
||||||
super.attachBaseContext(base)
|
appTheme = PreferencesHelper.getTheme(this)
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
MultiDex.install(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
|
||||||
super.onConfigurationChanged(newConfig)
|
return DaggerAppComponent.builder()
|
||||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
.appModule(AppModule(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
ACRA.init(this)
|
ACRA.init(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupJobManager() {
|
companion object {
|
||||||
JobManager.create(this).addJobCreator { tag ->
|
@JvmStatic
|
||||||
when (tag) {
|
fun get(context: Context): App {
|
||||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
return context.applicationContext as App
|
||||||
UpdaterJob.TAG -> UpdaterJob()
|
|
||||||
BackupCreatorJob.TAG -> BackupCreatorJob()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
|
||||||
Notifications.createChannels(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import rx.Observable
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.api.*
|
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
|
||||||
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
|
||||||
|
|
||||||
addSingleton(app)
|
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { ChapterCache(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { CoverCache(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
|
|
||||||
|
|
||||||
addSingletonFactory { ExtensionManager(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { DownloadManager(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
|
||||||
|
|
||||||
rxAsync { get<PreferencesHelper>() }
|
|
||||||
|
|
||||||
rxAsync { get<NetworkHelper>() }
|
|
||||||
|
|
||||||
rxAsync { get<SourceManager>() }
|
|
||||||
|
|
||||||
rxAsync { get<DatabaseHelper>() }
|
|
||||||
|
|
||||||
rxAsync { get<DownloadManager>() }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rxAsync(block: () -> Unit) {
|
|
||||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
8
app/src/main/java/eu/kanade/tachiyomi/Constants.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
object Constants {
|
||||||
|
const val NOTIFICATION_LIBRARY_ID = 1
|
||||||
|
const val NOTIFICATION_UPDATER_ID = 2
|
||||||
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
||||||
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
||||||
|
}
|
@ -1,63 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Migrations {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a migration when the application is updated.
|
|
||||||
*
|
|
||||||
* @param preferences Preferences of the application.
|
|
||||||
* @return true if a migration is performed, false otherwise.
|
|
||||||
*/
|
|
||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
|
||||||
val context = preferences.context
|
|
||||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
|
||||||
|
|
||||||
if (oldVersion == 0) return false
|
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
|
||||||
// Restore jobs after upgrading to evernote's job scheduler.
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
|
||||||
UpdaterJob.setupTask()
|
|
||||||
}
|
|
||||||
LibraryUpdateJob.setupTask()
|
|
||||||
}
|
|
||||||
if (oldVersion < 15) {
|
|
||||||
// Delete internal chapter cache dir.
|
|
||||||
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
|
||||||
}
|
|
||||||
if (oldVersion < 19) {
|
|
||||||
// Move covers to external files dir.
|
|
||||||
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
|
||||||
if (oldDir.exists()) {
|
|
||||||
val destDir = context.getExternalFilesDir("covers")
|
|
||||||
if (destDir != null) {
|
|
||||||
oldDir.listFiles().forEach {
|
|
||||||
it.renameTo(File(destDir, it.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 26) {
|
|
||||||
// Delete external chapter cache dir.
|
|
||||||
val extCache = context.externalCacheDir
|
|
||||||
if (extCache != null) {
|
|
||||||
val chapterCache = File(extCache, "chapter_disk_cache")
|
|
||||||
if (chapterCache.exists()) {
|
|
||||||
chapterCache.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
|
|
||||||
|
|
||||||
object BackupConst {
|
|
||||||
|
|
||||||
const val INTENT_FILTER = "SettingsBackupFragment"
|
|
||||||
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
|
||||||
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
|
||||||
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
|
||||||
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
|
||||||
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
|
||||||
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
|
||||||
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
|
||||||
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
|
||||||
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
|
||||||
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
|
||||||
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
|
||||||
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
|
||||||
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
|
||||||
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
|
||||||
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.app.IntentService
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [IntentService] used to backup [Manga] information to [JsonArray]
|
|
||||||
*/
|
|
||||||
class BackupCreateService : IntentService(NAME) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Name of class
|
|
||||||
private const val NAME = "BackupCreateService"
|
|
||||||
|
|
||||||
// Options for backup
|
|
||||||
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
|
||||||
|
|
||||||
// Filter options
|
|
||||||
internal const val BACKUP_CATEGORY = 0x1
|
|
||||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
|
||||||
internal const val BACKUP_CHAPTER = 0x2
|
|
||||||
internal const val BACKUP_CHAPTER_MASK = 0x2
|
|
||||||
internal const val BACKUP_HISTORY = 0x4
|
|
||||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
|
||||||
internal const val BACKUP_TRACK = 0x8
|
|
||||||
internal const val BACKUP_TRACK_MASK = 0x8
|
|
||||||
internal const val BACKUP_ALL = 0xF
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a backup from library
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param flags determines what to backup
|
|
||||||
*/
|
|
||||||
fun makeBackup(context: Context, uri: Uri, flags: Int) {
|
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
|
||||||
putExtra(EXTRA_FLAGS, flags)
|
|
||||||
}
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private val backupManager by lazy { BackupManager(this) }
|
|
||||||
|
|
||||||
override fun onHandleIntent(intent: Intent?) {
|
|
||||||
if (intent == null) return
|
|
||||||
|
|
||||||
// Get values
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
|
||||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
|
||||||
// Create backup
|
|
||||||
backupManager.createBackup(uri, flags, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.evernote.android.job.Job
|
|
||||||
import com.evernote.android.job.JobManager
|
|
||||||
import com.evernote.android.job.JobRequest
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class BackupCreatorJob : Job() {
|
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
|
||||||
val backupManager = BackupManager(context)
|
|
||||||
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
|
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
|
||||||
backupManager.createBackup(uri, flags, true)
|
|
||||||
return Result.SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "BackupCreator"
|
|
||||||
|
|
||||||
fun setupTask(prefInterval: Int? = null) {
|
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
|
||||||
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
|
||||||
if (interval > 0) {
|
|
||||||
JobRequest.Builder(TAG)
|
|
||||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
|
||||||
.setUpdateCurrent(true)
|
|
||||||
.build()
|
|
||||||
.schedule()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelTask() {
|
|
||||||
JobManager.instance().cancelAllForTag(TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,308 +1,201 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import com.github.salomonbrys.kotson.*
|
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.hippo.unifile.UniFile
|
import com.google.gson.reflect.TypeToken
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.*
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import java.io.*
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import java.lang.reflect.Type
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import java.util.*
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
/**
|
||||||
|
* This class provides the necessary methods to create and restore backups for the data of the
|
||||||
|
* application. The backup follows a JSON structure, with the following scheme:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "mangas": [
|
||||||
|
* {
|
||||||
|
* "manga": {"id": 1, ...},
|
||||||
|
* "chapters": [{"id": 1, ...}, {...}],
|
||||||
|
* "sync": [{"id": 1, ...}, {...}],
|
||||||
|
* "categories": ["cat1", "cat2", ...]
|
||||||
|
* },
|
||||||
|
* { ... }
|
||||||
|
* ],
|
||||||
|
* "categories": [
|
||||||
|
* {"id": 1, ...},
|
||||||
|
* {"id": 2, ...}
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param db the database helper.
|
||||||
|
*/
|
||||||
|
class BackupManager(private val db: DatabaseHelper) {
|
||||||
|
|
||||||
|
private val MANGA = "manga"
|
||||||
|
private val MANGAS = "mangas"
|
||||||
|
private val CHAPTERS = "chapters"
|
||||||
|
private val MANGA_SYNC = "sync"
|
||||||
|
private val CATEGORIES = "categories"
|
||||||
|
|
||||||
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(Integer::class.java, IntegerSerializer())
|
||||||
|
.setExclusionStrategies(IdExclusion())
|
||||||
|
.create()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database.
|
* Backups the data of the application to a file.
|
||||||
*/
|
|
||||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
internal val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracking manager
|
|
||||||
*/
|
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version of parser
|
|
||||||
*/
|
|
||||||
var version: Int = version
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Json Parser
|
|
||||||
*/
|
|
||||||
var parser: Gson = initParser()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences
|
|
||||||
*/
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set version of parser
|
|
||||||
*
|
*
|
||||||
* @param version version of parser
|
* @param file the file where the backup will be saved.
|
||||||
|
* @throws IOException if there's any IO error.
|
||||||
*/
|
*/
|
||||||
internal fun setVersion(version: Int) {
|
@Throws(IOException::class)
|
||||||
this.version = version
|
fun backupToFile(file: File) {
|
||||||
parser = initParser()
|
val root = backupToJson()
|
||||||
}
|
|
||||||
|
|
||||||
private fun initParser(): Gson = when (version) {
|
FileWriter(file).use {
|
||||||
1 -> GsonBuilder().create()
|
gson.toJson(root, it)
|
||||||
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("Json version unknown")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create backup Json file from database
|
* Creates a JSON object containing the backup of the app's data.
|
||||||
*
|
*
|
||||||
* @param uri path of Uri
|
* @return the backup as a JSON object.
|
||||||
* @param isJob backup called from job
|
|
||||||
*/
|
*/
|
||||||
fun createBackup(uri: Uri, flags: Int, isJob: Boolean) {
|
fun backupToJson(): JsonObject {
|
||||||
// Create root object
|
|
||||||
val root = JsonObject()
|
val root = JsonObject()
|
||||||
|
|
||||||
// Create manga array
|
// Backup library mangas and its dependencies
|
||||||
val mangaEntries = JsonArray()
|
val mangaEntries = JsonArray()
|
||||||
|
root.add(MANGAS, mangaEntries)
|
||||||
|
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
|
||||||
|
mangaEntries.add(backupManga(manga))
|
||||||
|
}
|
||||||
|
|
||||||
// Create category array
|
// Backup categories
|
||||||
val categoryEntries = JsonArray()
|
val categoryEntries = JsonArray()
|
||||||
|
root.add(CATEGORIES, categoryEntries)
|
||||||
// Add value's to root
|
for (category in db.getCategories().executeAsBlocking()) {
|
||||||
root[Backup.VERSION] = Backup.CURRENT_VERSION
|
categoryEntries.add(backupCategory(category))
|
||||||
root[Backup.MANGAS] = mangaEntries
|
|
||||||
root[CATEGORIES] = categoryEntries
|
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
|
||||||
// Get manga from database
|
|
||||||
val mangas = getFavoriteManga()
|
|
||||||
|
|
||||||
// Backup library manga and its dependencies
|
|
||||||
mangas.forEach { manga ->
|
|
||||||
mangaEntries.add(backupMangaObject(manga, flags))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
|
||||||
backupCategories(categoryEntries)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return root
|
||||||
// When BackupCreatorJob
|
|
||||||
if (isJob) {
|
|
||||||
// Get dir of file and create
|
|
||||||
var dir = UniFile.fromUri(context, uri)
|
|
||||||
dir = dir.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = numberOfBackups()
|
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
|
||||||
.sortedByDescending { it.name }
|
|
||||||
.drop(numberOfBackups - 1)
|
|
||||||
.forEach { it.delete() }
|
|
||||||
|
|
||||||
// Create new file to place backup
|
|
||||||
val newFile = dir.createFile(Backup.getDefaultFilename())
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
|
|
||||||
newFile.openOutputStream().bufferedWriter().use {
|
|
||||||
parser.toJson(root, it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val file = UniFile.fromUri(context, uri)
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
file.openOutputStream().bufferedWriter().use {
|
|
||||||
parser.toJson(root, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show completed dialog
|
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
|
|
||||||
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
|
|
||||||
}
|
|
||||||
context.sendLocalBroadcast(intent)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
if (!isJob) {
|
|
||||||
// Show error dialog
|
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
|
|
||||||
}
|
|
||||||
context.sendLocalBroadcast(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup the categories of library
|
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
|
||||||
*
|
*
|
||||||
* @param root root of categories json
|
* @param manga the manga to backup.
|
||||||
|
* @return a JSON object containing all the data of the manga.
|
||||||
*/
|
*/
|
||||||
internal fun backupCategories(root: JsonArray) {
|
private fun backupManga(manga: Manga): JsonObject {
|
||||||
val categories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a manga to Json
|
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @return [JsonElement] containing manga information
|
|
||||||
*/
|
|
||||||
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
|
||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val entry = JsonObject()
|
val entry = JsonObject()
|
||||||
|
|
||||||
// Backup manga fields
|
// Backup manga fields
|
||||||
entry[MANGA] = parser.toJsonTree(manga)
|
entry.add(MANGA, gson.toJsonTree(manga))
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
// Backup all the chapters
|
||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||||
// Backup all the chapters
|
if (!chapters.isEmpty()) {
|
||||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
||||||
if (!chapters.isEmpty()) {
|
|
||||||
val chaptersJson = parser.toJsonTree(chapters)
|
|
||||||
if (chaptersJson.asJsonArray.size() > 0) {
|
|
||||||
entry[CHAPTERS] = chaptersJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
// Backup manga sync
|
||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
|
||||||
// Backup categories for this manga
|
if (!mangaSync.isEmpty()) {
|
||||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
|
||||||
if (!categoriesForManga.isEmpty()) {
|
|
||||||
val categoriesNames = categoriesForManga.map { it.name }
|
|
||||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
// Backup categories for this manga
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
if (!categoriesForManga.isEmpty()) {
|
||||||
if (!tracks.isEmpty()) {
|
val categoriesNames = ArrayList<String>()
|
||||||
entry[TRACK] = parser.toJsonTree(tracks)
|
for (category in categoriesForManga) {
|
||||||
}
|
categoriesNames.add(category.name)
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
|
||||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
|
||||||
if (!historyForManga.isEmpty()) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
/**
|
||||||
manga.id = dbManga.id
|
* Backups a category.
|
||||||
manga.copyFrom(dbManga)
|
*
|
||||||
manga.favorite = true
|
* @param category the category to backup.
|
||||||
insertManga(manga)
|
* @return a JSON object containing the data of the category.
|
||||||
|
*/
|
||||||
|
private fun backupCategory(category: Category): JsonElement {
|
||||||
|
return gson.toJsonTree(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Observable] that fetches manga information
|
* Restores a backup from a file.
|
||||||
*
|
*
|
||||||
* @param source source of manga
|
* @param file the file containing the backup.
|
||||||
* @param manga manga that needs updating
|
* @throws IOException if there's any IO error.
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
*/
|
||||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
@Throws(IOException::class)
|
||||||
return source.fetchMangaDetails(manga)
|
fun restoreFromFile(file: File) {
|
||||||
.map { networkManga ->
|
JsonReader(FileReader(file)).use {
|
||||||
manga.copyFrom(networkManga)
|
val root = JsonParser().parse(it).asJsonObject
|
||||||
manga.favorite = true
|
restoreFromJson(root)
|
||||||
manga.initialized = true
|
}
|
||||||
manga.id = insertManga(manga)
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Observable] that fetches chapter information
|
* Restores a backup from an input stream.
|
||||||
*
|
*
|
||||||
* @param source source of manga
|
* @param stream the stream containing the backup.
|
||||||
* @param manga manga that needs updating
|
* @throws IOException if there's any IO error.
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
*/
|
||||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
@Throws(IOException::class)
|
||||||
return source.fetchChapterList(manga)
|
fun restoreFromStream(stream: InputStream) {
|
||||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
JsonReader(InputStreamReader(stream)).use {
|
||||||
.doOnNext {
|
val root = JsonParser().parse(it).asJsonObject
|
||||||
if (it.first.isNotEmpty()) {
|
restoreFromJson(root)
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
}
|
||||||
insertChapters(chapters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restores a backup from a JSON object. Everything executes in a single transaction so that
|
||||||
|
* nothing is modified if there's an error.
|
||||||
*
|
*
|
||||||
* @param jsonCategories array containing categories
|
* @param root the root of the JSON.
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategories(jsonCategories: JsonArray) {
|
fun restoreFromJson(root: JsonObject) {
|
||||||
|
db.inTransaction {
|
||||||
|
// Restore categories
|
||||||
|
root.get(CATEGORIES)?.let {
|
||||||
|
restoreCategories(it.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore mangas
|
||||||
|
root.get(MANGAS)?.let {
|
||||||
|
restoreMangas(it.asJsonArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the categories.
|
||||||
|
*
|
||||||
|
* @param jsonCategories the categories of the json.
|
||||||
|
*/
|
||||||
|
private fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = db.getCategories().executeAsBlocking()
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
|
||||||
|
object : TypeToken<List<Category>>() {}.type)
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
backupCategories.forEach { category ->
|
for (category in backupCategories) {
|
||||||
// Used to know if the category is already in the db
|
// Used to know if the category is already in the db
|
||||||
var found = false
|
var found = false
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
@ -319,20 +212,106 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
// Let the db assign the id
|
// Let the db assign the id
|
||||||
category.id = null
|
category.id = null
|
||||||
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
val result = db.insertCategory(category).executeAsBlocking()
|
||||||
category.id = result.insertedId()?.toInt()
|
category.id = result.insertedId()?.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores all the mangas and its related data.
|
||||||
|
*
|
||||||
|
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
||||||
|
*/
|
||||||
|
private fun restoreMangas(jsonMangas: JsonArray) {
|
||||||
|
val chapterToken = object : TypeToken<List<Chapter>>() {}.type
|
||||||
|
val mangaSyncToken = object : TypeToken<List<MangaSync>>() {}.type
|
||||||
|
val categoriesNamesToken = object : TypeToken<List<String>>() {}.type
|
||||||
|
|
||||||
|
for (backupManga in jsonMangas) {
|
||||||
|
// Map every entry to objects
|
||||||
|
val element = backupManga.asJsonObject
|
||||||
|
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
|
||||||
|
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
|
||||||
|
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
|
||||||
|
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
|
||||||
|
|
||||||
|
// Restore everything related to this manga
|
||||||
|
restoreManga(manga)
|
||||||
|
restoreChaptersForManga(manga, chapters)
|
||||||
|
restoreSyncForManga(manga, sync)
|
||||||
|
restoreCategoriesForManga(manga, categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to restore.
|
||||||
|
*/
|
||||||
|
private fun restoreManga(manga: Manga) {
|
||||||
|
// Try to find existing manga in db
|
||||||
|
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Let the db assign the id
|
||||||
|
manga.id = null
|
||||||
|
val result = db.insertManga(manga).executeAsBlocking()
|
||||||
|
manga.id = result.insertedId()
|
||||||
|
} else {
|
||||||
|
// If it exists already, we copy only the values related to the source from the db
|
||||||
|
// (they can be up to date). Local values (flags) are kept from the backup.
|
||||||
|
manga.id = dbManga.id
|
||||||
|
manga.copyFrom(dbManga)
|
||||||
|
manga.favorite = true
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the chapters of a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose chapters have to be restored.
|
||||||
|
* @param chapters the chapters to restore.
|
||||||
|
*/
|
||||||
|
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
for (chapter in chapters) {
|
||||||
|
chapter.manga_id = manga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
val chaptersToUpdate = ArrayList<Chapter>()
|
||||||
|
for (backupChapter in chapters) {
|
||||||
|
// Try to find existing chapter in db
|
||||||
|
val pos = dbChapters.indexOf(backupChapter)
|
||||||
|
if (pos != -1) {
|
||||||
|
// The chapter is already in the db, only update its fields
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
// If one of them was read, the chapter will be marked as read
|
||||||
|
dbChapter.read = backupChapter.read || dbChapter.read
|
||||||
|
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
|
||||||
|
chaptersToUpdate.add(dbChapter)
|
||||||
|
} else {
|
||||||
|
// Insert new chapter. Let the db assign the id
|
||||||
|
backupChapter.id = null
|
||||||
|
chaptersToUpdate.add(backupChapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (!chaptersToUpdate.isEmpty()) {
|
||||||
|
db.insertChapters(chaptersToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the categories a manga is in.
|
* Restores the categories a manga is in.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose categories have to be restored.
|
* @param manga the manga whose categories have to be restored.
|
||||||
* @param categories the categories to restore.
|
* @param categories the categories to restore.
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = db.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
@ -347,152 +326,56 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
if (!mangaCategoriesToUpdate.isEmpty()) {
|
||||||
val mangaAsList = ArrayList<Manga>()
|
val mangaAsList = ArrayList<Manga>()
|
||||||
mangaAsList.add(manga)
|
mangaAsList.add(manga)
|
||||||
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore history from Json
|
|
||||||
*
|
|
||||||
* @param history list containing history to be restored
|
|
||||||
*/
|
|
||||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
|
||||||
// List containing history to be updated
|
|
||||||
val historyToBeUpdated = ArrayList<History>()
|
|
||||||
for ((url, lastRead) in history) {
|
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
|
||||||
// Check if history already in database and update
|
|
||||||
if (dbHistory != null) {
|
|
||||||
dbHistory.apply {
|
|
||||||
last_read = Math.max(lastRead, dbHistory.last_read)
|
|
||||||
}
|
|
||||||
historyToBeUpdated.add(dbHistory)
|
|
||||||
} else {
|
|
||||||
// If not in database create
|
|
||||||
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
|
||||||
val historyToAdd = History.create(it).apply {
|
|
||||||
last_read = lastRead
|
|
||||||
}
|
|
||||||
historyToBeUpdated.add(historyToAdd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the sync of a manga.
|
* Restores the sync of a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose sync have to be restored.
|
* @param manga the manga whose sync have to be restored.
|
||||||
* @param tracks the track list to restore.
|
* @param sync the sync to restore.
|
||||||
*/
|
*/
|
||||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
tracks.map { it.manga_id = manga.id!! }
|
for (mangaSync in sync) {
|
||||||
|
mangaSync.manga_id = manga.id
|
||||||
// Get tracks from database
|
|
||||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
val trackToUpdate = ArrayList<Track>()
|
|
||||||
|
|
||||||
for (track in tracks) {
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
var isInDatabase = false
|
|
||||||
for (dbTrack in dbTracks) {
|
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
|
||||||
// The sync is already in the db, only update its fields
|
|
||||||
if (track.media_id != dbTrack.media_id) {
|
|
||||||
dbTrack.media_id = track.media_id
|
|
||||||
}
|
|
||||||
if (track.library_id != dbTrack.library_id) {
|
|
||||||
dbTrack.library_id = track.library_id
|
|
||||||
}
|
|
||||||
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
|
||||||
isInDatabase = true
|
|
||||||
trackToUpdate.add(dbTrack)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
track.id = null
|
|
||||||
trackToUpdate.add(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Update database
|
|
||||||
if (!trackToUpdate.isEmpty()) {
|
|
||||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
||||||
* Restore the chapters for manga if chapters already in database
|
val syncToUpdate = ArrayList<MangaSync>()
|
||||||
*
|
for (backupSync in sync) {
|
||||||
* @param manga manga of chapters
|
// Try to find existing chapter in db
|
||||||
* @param chapters list containing chapters that get restored
|
val pos = dbSyncs.indexOf(backupSync)
|
||||||
* @return boolean answering if chapter fetch is not needed
|
|
||||||
*/
|
|
||||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
// Return if fetch is needed
|
|
||||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
|
|
||||||
return false
|
|
||||||
|
|
||||||
for (chapter in chapters) {
|
|
||||||
val pos = dbChapters.indexOf(chapter)
|
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
val dbChapter = dbChapters[pos]
|
// The sync is already in the db, only update its fields
|
||||||
chapter.id = dbChapter.id
|
val dbSync = dbSyncs[pos]
|
||||||
chapter.copyFrom(dbChapter)
|
// Mark the max chapter as read and nothing else
|
||||||
break
|
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
|
||||||
|
syncToUpdate.add(dbSync)
|
||||||
|
} else {
|
||||||
|
// Insert new sync. Let the db assign the id
|
||||||
|
backupSync.id = null
|
||||||
|
syncToUpdate.add(backupSync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
chapters.filter { it.id != null }
|
|
||||||
chapters.map { it.manga_id = manga.id }
|
|
||||||
|
|
||||||
insertChapters(chapters)
|
// Update database
|
||||||
return true
|
if (!syncToUpdate.isEmpty()) {
|
||||||
|
db.insertMangasSync(syncToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns a list of items from a json element, or an empty list if the element is null.
|
||||||
*
|
*
|
||||||
* @return [Manga], null if not found
|
* @param element the json to be mapped to a list of items.
|
||||||
|
* @param type the gson mapping to restore the list.
|
||||||
|
* @return a list of items.
|
||||||
*/
|
*/
|
||||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
|
||||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns list containing manga from library
|
|
||||||
*
|
|
||||||
* @return [Manga] from library
|
|
||||||
*/
|
|
||||||
internal fun getFavoriteManga(): List<Manga> =
|
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts manga and returns id
|
|
||||||
*
|
|
||||||
* @return id of [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun insertManga(manga: Manga): Long? =
|
|
||||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts list of chapters
|
|
||||||
*/
|
|
||||||
private fun insertChapters(chapters: List<Chapter>) {
|
|
||||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return number of backups.
|
|
||||||
*
|
|
||||||
* @return number of backups selected by user
|
|
||||||
*/
|
|
||||||
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
|
|
||||||
}
|
}
|
||||||
|
@ -1,445 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.PowerManager
|
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.util.chop
|
|
||||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores backup from json file
|
|
||||||
*/
|
|
||||||
class BackupRestoreService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the status of the service.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
* @return true if the service is running, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun isRunning(context: Context): Boolean =
|
|
||||||
context.isServiceRunning(BackupRestoreService::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a service to restore a backup from Json
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @param uri path of Uri
|
|
||||||
*/
|
|
||||||
fun start(context: Context, uri: Uri) {
|
|
||||||
if (!isRunning(context)) {
|
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
|
||||||
}
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the service.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
fun stop(context: Context) {
|
|
||||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake lock that will be held until the service is destroyed.
|
|
||||||
*/
|
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the update is done.
|
|
||||||
*/
|
|
||||||
private var subscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The progress of a backup restore
|
|
||||||
*/
|
|
||||||
private var restoreProgress = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount of manga in Json file (needed for restore)
|
|
||||||
*/
|
|
||||||
private var restoreAmount = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List containing errors
|
|
||||||
*/
|
|
||||||
private val errors = mutableListOf<Pair<Date, String>>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup manager
|
|
||||||
*/
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database
|
|
||||||
*/
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracking manager
|
|
||||||
*/
|
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
|
|
||||||
private lateinit var executor: ExecutorService
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
|
||||||
*/
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
|
||||||
wakeLock.acquire()
|
|
||||||
executor = Executors.newSingleThreadExecutor()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called when the service is destroyed. It destroys the running subscription and
|
|
||||||
* releases the wake lock.
|
|
||||||
*/
|
|
||||||
override fun onDestroy() {
|
|
||||||
subscription?.unsubscribe()
|
|
||||||
executor.shutdown() // must be called after unsubscribe
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
wakeLock.release()
|
|
||||||
}
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method needs to be implemented, but it's not used/needed.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called when the service receives an intent.
|
|
||||||
*
|
|
||||||
* @param intent the start intent from.
|
|
||||||
* @param flags the flags of the command.
|
|
||||||
* @param startId the start id of this command.
|
|
||||||
* @return the start value of the command.
|
|
||||||
*/
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
|
||||||
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
|
||||||
subscription?.unsubscribe()
|
|
||||||
|
|
||||||
subscription = Observable.using(
|
|
||||||
{ db.lowLevel().beginTransaction() },
|
|
||||||
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
|
|
||||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
|
||||||
.doAfterTerminate { stopSelf(startId) }
|
|
||||||
.subscribeOn(Schedulers.from(executor))
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an [Observable] containing restore process.
|
|
||||||
*
|
|
||||||
* @param uri restore file
|
|
||||||
* @return [Observable<Manga>]
|
|
||||||
*/
|
|
||||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
return Observable.just(Unit)
|
|
||||||
.map {
|
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
|
||||||
val json = JsonParser().parse(reader).asJsonObject
|
|
||||||
|
|
||||||
// Get parser version
|
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
|
||||||
|
|
||||||
// Initialize manager
|
|
||||||
backupManager = BackupManager(this, version)
|
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
|
||||||
|
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
|
||||||
restoreProgress = 0
|
|
||||||
errors.clear()
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
json.get(CATEGORIES)?.let {
|
|
||||||
backupManager.restoreCategories(it.asJsonArray)
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
mangasJson
|
|
||||||
}
|
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.concatMap {
|
|
||||||
val obj = it.asJsonObject
|
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
|
||||||
|
|
||||||
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
|
||||||
if (observable != null) {
|
|
||||||
observable
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
|
||||||
restoreProgress += 1
|
|
||||||
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
|
||||||
Observable.just(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
.doOnNext {
|
|
||||||
val endTime = System.currentTimeMillis()
|
|
||||||
val time = endTime - startTime
|
|
||||||
val logFile = writeErrorLog()
|
|
||||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_TIME, time)
|
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(completeIntent)
|
|
||||||
|
|
||||||
}
|
|
||||||
.doOnError { error ->
|
|
||||||
Timber.e(error)
|
|
||||||
writeErrorLog()
|
|
||||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(errorIntent)
|
|
||||||
}
|
|
||||||
.onErrorReturn { emptyList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write errors to error log
|
|
||||||
*/
|
|
||||||
private fun writeErrorLog(): File {
|
|
||||||
try {
|
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
|
||||||
errors.forEach { (date, message) ->
|
|
||||||
out.write("[${sdf.format(date)}] $message\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return destFile
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Empty
|
|
||||||
}
|
|
||||||
return File("")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a manga restore observable
|
|
||||||
*
|
|
||||||
* @param manga manga data from json
|
|
||||||
* @param chapters chapters data from json
|
|
||||||
* @param categories categories data from json
|
|
||||||
* @param history history data from json
|
|
||||||
* @param tracks tracking data from json
|
|
||||||
* @return [Observable] containing manga restore information
|
|
||||||
*/
|
|
||||||
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
|
||||||
categories: List<String>, history: List<DHistory>,
|
|
||||||
tracks: List<Track>): Observable<Manga>? {
|
|
||||||
// Get source
|
|
||||||
val source = backupManager.sourceManager.getOrStub(manga.source)
|
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
|
|
||||||
return if (dbManga == null) {
|
|
||||||
// Manga not in database
|
|
||||||
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
|
||||||
} else { // Manga in database
|
|
||||||
// Copy information from manga already in database
|
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
|
||||||
// Fetch rest of manga information
|
|
||||||
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters chapters of manga that needs updating
|
|
||||||
* @param categories categories that need updating
|
|
||||||
*/
|
|
||||||
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
|
||||||
categories: List<String>, history: List<DHistory>,
|
|
||||||
tracks: List<Track>): Observable<Manga> {
|
|
||||||
return backupManager.restoreMangaFetchObservable(source, manga)
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.filter { it.id != null }
|
|
||||||
.flatMap {
|
|
||||||
chapterFetchObservable(source, it, chapters)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
trackingFetchObservable(it, tracks)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
|
||||||
categories: List<String>, history: List<DHistory>,
|
|
||||||
tracks: List<Track>): Observable<Manga> {
|
|
||||||
|
|
||||||
return Observable.just(backupManga)
|
|
||||||
.flatMap { manga ->
|
|
||||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
|
||||||
chapterFetchObservable(source, manga, chapters)
|
|
||||||
.map { manga }
|
|
||||||
} else {
|
|
||||||
Observable.just(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap { manga ->
|
|
||||||
trackingFetchObservable(manga, tracks)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
|
||||||
// Restore categories
|
|
||||||
backupManager.restoreCategoriesForManga(manga, categories)
|
|
||||||
|
|
||||||
// Restore history
|
|
||||||
backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches chapter information
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
|
||||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
|
||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
|
||||||
// If there's any error, return empty update and continue.
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
Pair(emptyList(), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that refreshes tracking information
|
|
||||||
* @param manga manga that needs updating.
|
|
||||||
* @param tracks list containing tracks from restore file.
|
|
||||||
* @return [Observable] that contains updated track item
|
|
||||||
*/
|
|
||||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
|
||||||
return Observable.from(tracks)
|
|
||||||
.concatMap { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
service.refresh(track)
|
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
track
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
|
|
||||||
Observable.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update dialog in [BackupConst]
|
|
||||||
*
|
|
||||||
* @param progress restore progress
|
|
||||||
* @param amount total restoreAmount of manga
|
|
||||||
* @param title title of restored manga
|
|
||||||
*/
|
|
||||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
|
||||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
|
||||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
|
||||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
|
||||||
|
|
||||||
data class DHistory(val url: String,val lastRead: Long)
|
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.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) {
|
|
||||||
val name = nextName()
|
|
||||||
|
|
||||||
when (name) {
|
|
||||||
URL -> chapter.url = nextString()
|
|
||||||
READ -> chapter.read = nextInt() == 1
|
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
|
||||||
LAST_READ -> chapter.last_page_read = nextInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.google.gson.ExclusionStrategy
|
||||||
|
import com.google.gson.FieldAttributes
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
|
||||||
|
class IdExclusion : ExclusionStrategy {
|
||||||
|
|
||||||
|
private val categoryExclusions = listOf("id")
|
||||||
|
private val mangaExclusions = listOf("id")
|
||||||
|
private val chapterExclusions = listOf("id", "manga_id")
|
||||||
|
private val syncExclusions = listOf("id", "manga_id", "update")
|
||||||
|
|
||||||
|
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
||||||
|
Manga::class.java -> mangaExclusions.contains(f.name)
|
||||||
|
Chapter::class.java -> chapterExclusions.contains(f.name)
|
||||||
|
MangaSync::class.java -> syncExclusions.contains(f.name)
|
||||||
|
Category::class.java -> categoryExclusions.contains(f.name)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldSkipClass(clazz: Class<*>) = false
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonPrimitive
|
||||||
|
import com.google.gson.JsonSerializationContext
|
||||||
|
import com.google.gson.JsonSerializer
|
||||||
|
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
class IntegerSerializer : JsonSerializer<Int> {
|
||||||
|
|
||||||
|
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
|
||||||
|
if (value != null && value !== 0)
|
||||||
|
return JsonPrimitive(value)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import android.telecom.DisconnectCause.REMOTE
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.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) {
|
|
||||||
val name = nextName()
|
|
||||||
|
|
||||||
when (name) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,19 +2,18 @@ package eu.kanade.tachiyomi.data.cache
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.util.DiskUtils
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.saveImageTo
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Okio
|
import okio.Okio
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create chapter cache
|
* Class used to create chapter cache
|
||||||
@ -27,6 +26,15 @@ import java.io.IOException
|
|||||||
*/
|
*/
|
||||||
class ChapterCache(private val context: Context) {
|
class ChapterCache(private val context: Context) {
|
||||||
|
|
||||||
|
/** Google Json class used for parsing JSON files. */
|
||||||
|
private val gson: Gson = Gson()
|
||||||
|
|
||||||
|
/** Cache class used for cache management. */
|
||||||
|
private val diskCache: DiskLruCache
|
||||||
|
|
||||||
|
/** Page list collection used for deserializing from JSON. */
|
||||||
|
private val pageListCollection: Type = object : TypeToken<List<Page>>() {}.type
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Name of cache directory. */
|
/** Name of cache directory. */
|
||||||
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
||||||
@ -41,36 +49,38 @@ class ChapterCache(private val context: Context) {
|
|||||||
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
|
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Google Json class used for parsing JSON files. */
|
init {
|
||||||
private val gson: Gson by injectLazy()
|
// Open cache in default cache directory.
|
||||||
|
diskCache = DiskLruCache.open(
|
||||||
/** Cache class used for cache management. */
|
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_VALUE_COUNT,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_CACHE_SIZE)
|
||||||
PARAMETER_CACHE_SIZE)
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns directory of cache.
|
* Returns directory of cache.
|
||||||
|
* @return directory of cache.
|
||||||
*/
|
*/
|
||||||
val cacheDir: File
|
val cacheDir: File
|
||||||
get() = diskCache.directory
|
get() = diskCache.directory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns real size of directory.
|
* Returns real size of directory.
|
||||||
|
* @return real size of directory.
|
||||||
*/
|
*/
|
||||||
private val realSize: Long
|
private val realSize: Long
|
||||||
get() = DiskUtil.getDirectorySize(cacheDir)
|
get() = DiskUtils.getDirectorySize(cacheDir)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns real size of directory in human readable format.
|
* Returns real size of directory in human readable format.
|
||||||
|
* @return real size of directory.
|
||||||
*/
|
*/
|
||||||
val readableSize: String
|
val readableSize: String
|
||||||
get() = Formatter.formatFileSize(context, realSize)
|
get() = Formatter.formatFileSize(context, realSize)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove file from cache.
|
* Remove file from cache.
|
||||||
*
|
|
||||||
* @param file name of file "md5.0".
|
* @param file name of file "md5.0".
|
||||||
* @return status of deletion for the file.
|
* @return status of deletion for the file.
|
||||||
*/
|
*/
|
||||||
@ -81,39 +91,37 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Remove the extension from the file to get the key of the cache
|
// Remove the extension from the file to get the key of the cache
|
||||||
val key = file.substringBeforeLast(".")
|
val key = file.substring(0, file.lastIndexOf("."))
|
||||||
// Remove file from cache.
|
// Remove file from cache.
|
||||||
return diskCache.remove(key)
|
return diskCache.remove(key)
|
||||||
} catch (e: Exception) {
|
} catch (e: IOException) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page list from cache.
|
* Get page list from cache.
|
||||||
*
|
* @param chapterUrl the url of the chapter.
|
||||||
* @param chapter the chapter.
|
|
||||||
* @return an observable of the list of pages.
|
* @return an observable of the list of pages.
|
||||||
*/
|
*/
|
||||||
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable<List<Page>> {
|
||||||
// Get the key for the chapter.
|
// Get the key for the chapter.
|
||||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
val key = DiskUtils.hashKeyForDisk(chapterUrl)
|
||||||
|
|
||||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
diskCache.get(key).use {
|
diskCache.get(key).use {
|
||||||
gson.fromJson<List<Page>>(it.getString(0))
|
gson.fromJson(it.getString(0), pageListCollection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add page list to disk cache.
|
* Add page list to disk cache.
|
||||||
*
|
* @param chapterUrl the url of the chapter.
|
||||||
* @param chapter the chapter.
|
|
||||||
* @param pages list of pages.
|
* @param pages list of pages.
|
||||||
*/
|
*/
|
||||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
|
||||||
// Convert list of pages to json string.
|
// Convert list of pages to json string.
|
||||||
val cachedValue = gson.toJson(pages)
|
val cachedValue = gson.toJson(pages)
|
||||||
|
|
||||||
@ -122,7 +130,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get editor from md5 key.
|
// Get editor from md5 key.
|
||||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
val key = DiskUtils.hashKeyForDisk(chapterUrl)
|
||||||
editor = diskCache.edit(key) ?: return
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
// Write chapter urls to cache.
|
// Write chapter urls to cache.
|
||||||
@ -143,61 +151,59 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if image is in cache.
|
* Check if image is in cache.
|
||||||
*
|
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @return true if in cache otherwise false.
|
* @return true if in cache otherwise false.
|
||||||
*/
|
*/
|
||||||
fun isImageInCache(imageUrl: String): Boolean {
|
fun isImageInCache(imageUrl: String): Boolean {
|
||||||
try {
|
try {
|
||||||
return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get image file from url.
|
* Get image path from url.
|
||||||
*
|
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @return path of image.
|
* @return path of image.
|
||||||
*/
|
*/
|
||||||
fun getImageFile(imageUrl: String): File {
|
fun getImagePath(imageUrl: String): String? {
|
||||||
// Get file from md5 key.
|
try {
|
||||||
val imageName = DiskUtil.hashKeyForDisk(imageUrl) + ".0"
|
// Get file from md5 key.
|
||||||
return File(diskCache.directory, imageName)
|
val imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0"
|
||||||
|
return File(diskCache.directory, imageName).canonicalPath
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add image to cache.
|
* Add image to cache.
|
||||||
*
|
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @param response http response from page.
|
* @param response http response from page.
|
||||||
* @throws IOException image error.
|
* @throws IOException image error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun putImageToCache(imageUrl: String, response: Response) {
|
fun putImageToCache(imageUrl: String, response: Response, reencode: Boolean) {
|
||||||
// Initialize editor (edits the values for an entry).
|
// Initialize editor (edits the values for an entry).
|
||||||
var editor: DiskLruCache.Editor? = null
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get editor from md5 key.
|
// Get editor from md5 key.
|
||||||
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
val key = DiskUtils.hashKeyForDisk(imageUrl)
|
||||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||||
|
|
||||||
// Get OutputStream and write image with Okio.
|
// Get OutputStream and write image with Okio.
|
||||||
response.body()!!.source().saveTo(editor.newOutputStream(0))
|
response.body().source().saveImageTo(editor.newOutputStream(0), reencode)
|
||||||
|
|
||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
} finally {
|
} finally {
|
||||||
response.body()?.close()
|
response.body().close()
|
||||||
editor?.abortUnlessCommitted()
|
editor?.abortUnlessCommitted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getKey(chapter: Chapter): String {
|
|
||||||
return "${chapter.manga_id}${chapter.url}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -20,8 +20,7 @@ class CoverCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Cache directory used for cache management.
|
* Cache directory used for cache management.
|
||||||
*/
|
*/
|
||||||
private val cacheDir = context.getExternalFilesDir("covers") ?:
|
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
||||||
File(context.filesDir, "covers").also { it.mkdirs() }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
@ -30,7 +29,7 @@ class CoverCache(private val context: Context) {
|
|||||||
* @return cover image.
|
* @return cover image.
|
||||||
*/
|
*/
|
||||||
fun getCoverFile(thumbnailUrl: String): File {
|
fun getCoverFile(thumbnailUrl: String): File {
|
||||||
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl))
|
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import eu.kanade.tachiyomi.data.database.queries.*
|
import eu.kanade.tachiyomi.data.database.queries.*
|
||||||
|
|
||||||
@ -10,20 +9,17 @@ import eu.kanade.tachiyomi.data.database.queries.*
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context)
|
||||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(DbOpenHelper(context))
|
||||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
|
||||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
|
||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|
||||||
fun lowLevel() = db.lowLevel()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,23 +3,23 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
|
||||||
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
||||||
lowLevel().beginTransaction()
|
internal().beginTransaction()
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
lowLevel().setTransactionSuccessful()
|
internal().setTransactionSuccessful()
|
||||||
} finally {
|
} finally {
|
||||||
lowLevel().endTransaction()
|
internal().endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
||||||
lowLevel().beginTransaction()
|
internal().beginTransaction()
|
||||||
try {
|
try {
|
||||||
val result = block()
|
val result = block()
|
||||||
lowLevel().setTransactionSuccessful()
|
internal().setTransactionSuccessful()
|
||||||
return result
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
lowLevel().endTransaction()
|
internal().endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,22 +17,20 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 7
|
const val DATABASE_VERSION = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
execSQL(MangaTable.createTableQuery)
|
execSQL(MangaTable.createTableQuery)
|
||||||
execSQL(ChapterTable.createTableQuery)
|
execSQL(ChapterTable.createTableQuery)
|
||||||
execSQL(TrackTable.createTableQuery)
|
execSQL(MangaSyncTable.createTableQuery)
|
||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
execSQL(HistoryTable.createTableQuery)
|
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
execSQL(MangaTable.createFavoriteIndexQuery)
|
execSQL(MangaTable.createFavoriteIndexQuery)
|
||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
@ -43,23 +41,6 @@ class DbOpenHelper(context: Context)
|
|||||||
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
||||||
}
|
}
|
||||||
if (oldVersion < 3) {
|
|
||||||
// Initialize history tables
|
|
||||||
db.execSQL(HistoryTable.createTableQuery)
|
|
||||||
db.execSQL(HistoryTable.createChapterIdIndexQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 4) {
|
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 5) {
|
|
||||||
db.execSQL(ChapterTable.addScanlator)
|
|
||||||
}
|
|
||||||
if (oldVersion < 6) {
|
|
||||||
db.execSQL(TrackTable.addTrackingUrl)
|
|
||||||
}
|
|
||||||
if (oldVersion < 7) {
|
|
||||||
db.execSQL(TrackTable.addLibraryId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
|
||||||
|
|
||||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
|
||||||
CategoryPutResolver(),
|
|
||||||
CategoryGetResolver(),
|
|
||||||
CategoryDeleteResolver()
|
|
||||||
)
|
|
||||||
|
|
||||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
|
||||||
put(COL_ID, obj.id)
|
|
||||||
put(COL_NAME, obj.name)
|
|
||||||
put(COL_ORDER, obj.order)
|
|
||||||
put(COL_FLAGS, obj.flags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
|
||||||
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
|
||||||
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
|
||||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_BOOKMARK
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
|
||||||
|
|
||||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
|
||||||
ChapterPutResolver(),
|
|
||||||
ChapterGetResolver(),
|
|
||||||
ChapterDeleteResolver()
|
|
||||||
)
|
|
||||||
|
|
||||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
|
||||||
put(COL_ID, obj.id)
|
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
|
||||||
put(COL_URL, obj.url)
|
|
||||||
put(COL_NAME, obj.name)
|
|
||||||
put(COL_READ, obj.read)
|
|
||||||
put(COL_SCANLATOR, obj.scanlator)
|
|
||||||
put(COL_BOOKMARK, obj.bookmark)
|
|
||||||
put(COL_DATE_FETCH, obj.date_fetch)
|
|
||||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
|
||||||
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
|
||||||
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
|
||||||
put(COL_SOURCE_ORDER, obj.source_order)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
|
||||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
|
||||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
|
||||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
|
||||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
|
||||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
|
||||||
|
|
||||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
|
||||||
HistoryPutResolver(),
|
|
||||||
HistoryGetResolver(),
|
|
||||||
HistoryDeleteResolver()
|
|
||||||
)
|
|
||||||
|
|
||||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
|
||||||
put(COL_ID, obj.id)
|
|
||||||
put(COL_CHAPTER_ID, obj.chapter_id)
|
|
||||||
put(COL_LAST_READ, obj.last_read)
|
|
||||||
put(COL_TIME_READ, obj.time_read)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
|
||||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
|
||||||
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
|
||||||
|
|
||||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
|
||||||
MangaCategoryPutResolver(),
|
|
||||||
MangaCategoryGetResolver(),
|
|
||||||
MangaCategoryDeleteResolver()
|
|
||||||
)
|
|
||||||
|
|
||||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
|
||||||
put(COL_ID, obj.id)
|
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
|
||||||
put(COL_CATEGORY_ID, obj.category_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
|
||||||
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
@ -1,102 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
|
||||||
|
|
||||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
|
||||||
MangaPutResolver(),
|
|
||||||
MangaGetResolver(),
|
|
||||||
MangaDeleteResolver()
|
|
||||||
)
|
|
||||||
|
|
||||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
|
||||||
put(COL_ID, obj.id)
|
|
||||||
put(COL_SOURCE, obj.source)
|
|
||||||
put(COL_URL, obj.url)
|
|
||||||
put(COL_ARTIST, obj.artist)
|
|
||||||
put(COL_AUTHOR, obj.author)
|
|
||||||
put(COL_DESCRIPTION, obj.description)
|
|
||||||
put(COL_GENRE, obj.genre)
|
|
||||||
put(COL_TITLE, obj.title)
|
|
||||||
put(COL_STATUS, obj.status)
|
|
||||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
|
||||||
put(COL_FAVORITE, obj.favorite)
|
|
||||||
put(COL_LAST_UPDATE, obj.last_update)
|
|
||||||
put(COL_INITIALIZED, obj.initialized)
|
|
||||||
put(COL_VIEWER, obj.viewer)
|
|
||||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseMangaGetResolver {
|
|
||||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
|
||||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
|
||||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
|
||||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
|
||||||
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
|
||||||
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
|
||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
|
||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
|
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
|
||||||
return mapBaseFromCursor(MangaImpl(), cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
|
||||||
|
|
||||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
|
||||||
TrackPutResolver(),
|
|
||||||
TrackGetResolver(),
|
|
||||||
TrackDeleteResolver()
|
|
||||||
)
|
|
||||||
|
|
||||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
|
||||||
put(COL_ID, obj.id)
|
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
|
||||||
put(COL_SYNC_ID, obj.sync_id)
|
|
||||||
put(COL_MEDIA_ID, obj.media_id)
|
|
||||||
put(COL_LIBRARY_ID, obj.library_id)
|
|
||||||
put(COL_TITLE, obj.title)
|
|
||||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
|
||||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
|
||||||
put(COL_STATUS, obj.status)
|
|
||||||
put(COL_TRACKING_URL, obj.tracking_url)
|
|
||||||
put(COL_SCORE, obj.score)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
|
||||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
|
||||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
|
||||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
|
||||||
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
|
||||||
.table(TABLE)
|
|
||||||
.where("$COL_ID = ?")
|
|
||||||
.whereArgs(obj.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
@ -0,0 +1,57 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||||
|
|
||||||
|
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
||||||
|
public class Category implements Serializable {
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = CategoryTable.COL_ID, key = true)
|
||||||
|
public Integer id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = CategoryTable.COL_NAME)
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = CategoryTable.COL_ORDER)
|
||||||
|
public int order;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = CategoryTable.COL_FLAGS)
|
||||||
|
public int flags;
|
||||||
|
|
||||||
|
public Category() {}
|
||||||
|
|
||||||
|
public static Category create(String name) {
|
||||||
|
Category c = new Category();
|
||||||
|
c.name = name;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Category createDefault() {
|
||||||
|
Category c = create("Default");
|
||||||
|
c.id = 0;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNameLower() {
|
||||||
|
return name.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Category category = (Category) o;
|
||||||
|
|
||||||
|
return name.equals(category.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return name.hashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface Category : Serializable {
|
|
||||||
|
|
||||||
var id: Int?
|
|
||||||
|
|
||||||
var name: String
|
|
||||||
|
|
||||||
var order: Int
|
|
||||||
|
|
||||||
var flags: Int
|
|
||||||
|
|
||||||
val nameLower: String
|
|
||||||
get() = name.toLowerCase()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
|
||||||
this.name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,94 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||||
|
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
|
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||||
|
|
||||||
|
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
||||||
|
public class Chapter implements Serializable {
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_ID, key = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_MANGA_ID)
|
||||||
|
public Long manga_id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_URL)
|
||||||
|
public String url;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_NAME)
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_READ)
|
||||||
|
public boolean read;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_LAST_PAGE_READ)
|
||||||
|
public int last_page_read;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_FETCH)
|
||||||
|
public long date_fetch;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_UPLOAD)
|
||||||
|
public long date_upload;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_CHAPTER_NUMBER)
|
||||||
|
public float chapter_number;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = ChapterTable.COL_SOURCE_ORDER)
|
||||||
|
public int source_order;
|
||||||
|
|
||||||
|
public int status;
|
||||||
|
|
||||||
|
private transient List<Page> pages;
|
||||||
|
|
||||||
|
public Chapter() {}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = UrlUtil.getPath(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Chapter chapter = (Chapter) o;
|
||||||
|
|
||||||
|
return url.equals(chapter.url);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return url.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Chapter create() {
|
||||||
|
Chapter chapter = new Chapter();
|
||||||
|
chapter.chapter_number = -1;
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Page> getPages() {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPages(List<Page> pages) {
|
||||||
|
this.pages = pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDownloaded() {
|
||||||
|
return status == Download.DOWNLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRecognizedNumber() {
|
||||||
|
return chapter_number >= 0f;
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface Chapter : SChapter, Serializable {
|
|
||||||
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
var manga_id: Long?
|
|
||||||
|
|
||||||
var read: Boolean
|
|
||||||
|
|
||||||
var bookmark: Boolean
|
|
||||||
|
|
||||||
var last_page_read: Int
|
|
||||||
|
|
||||||
var date_fetch: Long
|
|
||||||
|
|
||||||
var source_order: Int
|
|
||||||
|
|
||||||
val isRecognizedNumber: Boolean
|
|
||||||
get() = chapter_number >= 0f
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(): Chapter = ChapterImpl().apply {
|
|
||||||
chapter_number = -1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
class ChapterImpl : Chapter {
|
|
||||||
|
|
||||||
override var id: Long? = null
|
|
||||||
|
|
||||||
override var manga_id: Long? = null
|
|
||||||
|
|
||||||
override lateinit var url: String
|
|
||||||
|
|
||||||
override lateinit var name: String
|
|
||||||
|
|
||||||
override var scanlator: String? = null
|
|
||||||
|
|
||||||
override var read: Boolean = false
|
|
||||||
|
|
||||||
override var bookmark: Boolean = false
|
|
||||||
|
|
||||||
override var last_page_read: Int = 0
|
|
||||||
|
|
||||||
override var date_fetch: Long = 0
|
|
||||||
|
|
||||||
override var date_upload: Long = 0
|
|
||||||
|
|
||||||
override var chapter_number: Float = 0f
|
|
||||||
|
|
||||||
override var source_order: Int = 0
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
|
||||||
|
|
||||||
val chapter = other as Chapter
|
|
||||||
return url == chapter.url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return url.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing the history statistics of a chapter
|
|
||||||
*/
|
|
||||||
interface History : Serializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of history object.
|
|
||||||
*/
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter id of history object.
|
|
||||||
*/
|
|
||||||
var chapter_id: Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last time chapter was read in time long format
|
|
||||||
*/
|
|
||||||
var last_read: Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total time chapter was read - todo not yet implemented
|
|
||||||
*/
|
|
||||||
var time_read: Long
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* History constructor
|
|
||||||
*
|
|
||||||
* @param chapter chapter object
|
|
||||||
* @return history object
|
|
||||||
*/
|
|
||||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
|
||||||
this.chapter_id = chapter.id!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing the history statistics of a chapter
|
|
||||||
*/
|
|
||||||
class HistoryImpl : History {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of history object.
|
|
||||||
*/
|
|
||||||
override var id: Long? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter id of history object.
|
|
||||||
*/
|
|
||||||
override var chapter_id: Long = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last time chapter was read in time long format
|
|
||||||
*/
|
|
||||||
override var last_read: Long = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total time chapter was read - todo not yet implemented
|
|
||||||
*/
|
|
||||||
override var time_read: Long = 0
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
class LibraryManga : MangaImpl() {
|
|
||||||
|
|
||||||
var unread: Int = 0
|
|
||||||
|
|
||||||
var category: Int = 0
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,213 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.R;
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||||
|
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||||
|
|
||||||
|
@StorIOSQLiteType(table = MangaTable.TABLE)
|
||||||
|
public class Manga implements Serializable {
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_ID, key = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_SOURCE)
|
||||||
|
public int source;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_URL)
|
||||||
|
public String url;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_ARTIST)
|
||||||
|
public String artist;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_AUTHOR)
|
||||||
|
public String author;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_DESCRIPTION)
|
||||||
|
public String description;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_GENRE)
|
||||||
|
public String genre;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_TITLE)
|
||||||
|
public String title;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_STATUS)
|
||||||
|
public int status;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_THUMBNAIL_URL)
|
||||||
|
public String thumbnail_url;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_FAVORITE)
|
||||||
|
public boolean favorite;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_LAST_UPDATE)
|
||||||
|
public long last_update;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_INITIALIZED)
|
||||||
|
public boolean initialized;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_VIEWER)
|
||||||
|
public int viewer;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaTable.COL_CHAPTER_FLAGS)
|
||||||
|
public int chapter_flags;
|
||||||
|
|
||||||
|
public transient int unread;
|
||||||
|
|
||||||
|
public transient int category;
|
||||||
|
|
||||||
|
public static final int UNKNOWN = 0;
|
||||||
|
public static final int ONGOING = 1;
|
||||||
|
public static final int COMPLETED = 2;
|
||||||
|
public static final int LICENSED = 3;
|
||||||
|
|
||||||
|
public static final int SORT_DESC = 0x00000000;
|
||||||
|
public static final int SORT_ASC = 0x00000001;
|
||||||
|
public static final int SORT_MASK = 0x00000001;
|
||||||
|
|
||||||
|
// Generic filter that does not filter anything
|
||||||
|
public static final int SHOW_ALL = 0x00000000;
|
||||||
|
|
||||||
|
public static final int SHOW_UNREAD = 0x00000002;
|
||||||
|
public static final int SHOW_READ = 0x00000004;
|
||||||
|
public static final int READ_MASK = 0x00000006;
|
||||||
|
|
||||||
|
public static final int SHOW_DOWNLOADED = 0x00000008;
|
||||||
|
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
||||||
|
public static final int DOWNLOADED_MASK = 0x00000018;
|
||||||
|
|
||||||
|
public static final int SORTING_SOURCE = 0x00000000;
|
||||||
|
public static final int SORTING_NUMBER = 0x00000100;
|
||||||
|
public static final int SORTING_MASK = 0x00000100;
|
||||||
|
|
||||||
|
public static final int DISPLAY_NAME = 0x00000000;
|
||||||
|
public static final int DISPLAY_NUMBER = 0x00100000;
|
||||||
|
public static final int DISPLAY_MASK = 0x00100000;
|
||||||
|
|
||||||
|
public Manga() {}
|
||||||
|
|
||||||
|
public static Manga create(String pathUrl) {
|
||||||
|
Manga m = new Manga();
|
||||||
|
m.url = pathUrl;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Manga create(String pathUrl, int source) {
|
||||||
|
Manga m = new Manga();
|
||||||
|
m.url = pathUrl;
|
||||||
|
m.source = source;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = UrlUtil.getPath(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyFrom(Manga other) {
|
||||||
|
if (other.title != null)
|
||||||
|
title = other.title;
|
||||||
|
|
||||||
|
if (other.author != null)
|
||||||
|
author = other.author;
|
||||||
|
|
||||||
|
if (other.artist != null)
|
||||||
|
artist = other.artist;
|
||||||
|
|
||||||
|
if (other.url != null)
|
||||||
|
url = other.url;
|
||||||
|
|
||||||
|
if (other.description != null)
|
||||||
|
description = other.description;
|
||||||
|
|
||||||
|
if (other.genre != null)
|
||||||
|
genre = other.genre;
|
||||||
|
|
||||||
|
if (other.thumbnail_url != null)
|
||||||
|
thumbnail_url = other.thumbnail_url;
|
||||||
|
|
||||||
|
status = other.status;
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus(Context context) {
|
||||||
|
switch (status) {
|
||||||
|
case ONGOING:
|
||||||
|
return context.getString(R.string.ongoing);
|
||||||
|
case COMPLETED:
|
||||||
|
return context.getString(R.string.completed);
|
||||||
|
case LICENSED:
|
||||||
|
return context.getString(R.string.licensed);
|
||||||
|
default:
|
||||||
|
return context.getString(R.string.unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChapterOrder(int order) {
|
||||||
|
setFlags(order, SORT_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayMode(int mode) {
|
||||||
|
setFlags(mode, DISPLAY_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadFilter(int filter) {
|
||||||
|
setFlags(filter, READ_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownloadedFilter(int filter) {
|
||||||
|
setFlags(filter, DOWNLOADED_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSorting(int sort) {
|
||||||
|
setFlags(sort, SORTING_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFlags(int flag, int mask) {
|
||||||
|
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sortDescending() {
|
||||||
|
return (chapter_flags & SORT_MASK) == SORT_DESC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to display the chapter's title one way or another
|
||||||
|
public int getDisplayMode() {
|
||||||
|
return chapter_flags & DISPLAY_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReadFilter() {
|
||||||
|
return chapter_flags & READ_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDownloadedFilter() {
|
||||||
|
return chapter_flags & DOWNLOADED_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSorting() {
|
||||||
|
return chapter_flags & SORTING_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Manga manga = (Manga) o;
|
||||||
|
|
||||||
|
return url.equals(manga.url);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return url.hashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -1,92 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
|
|
||||||
interface Manga : SManga {
|
|
||||||
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
var source: Long
|
|
||||||
|
|
||||||
var favorite: Boolean
|
|
||||||
|
|
||||||
var last_update: Long
|
|
||||||
|
|
||||||
var viewer: Int
|
|
||||||
|
|
||||||
var chapter_flags: Int
|
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
|
||||||
setFlags(order, SORT_MASK)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
|
||||||
var displayMode: Int
|
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
|
||||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
|
||||||
|
|
||||||
var readFilter: Int
|
|
||||||
get() = chapter_flags and READ_MASK
|
|
||||||
set(filter) = setFlags(filter, READ_MASK)
|
|
||||||
|
|
||||||
var downloadedFilter: Int
|
|
||||||
get() = chapter_flags and DOWNLOADED_MASK
|
|
||||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
|
||||||
|
|
||||||
var bookmarkedFilter: Int
|
|
||||||
get() = chapter_flags and BOOKMARKED_MASK
|
|
||||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
|
||||||
|
|
||||||
var sorting: Int
|
|
||||||
get() = chapter_flags and SORTING_MASK
|
|
||||||
set(sort) = setFlags(sort, SORTING_MASK)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val SORT_DESC = 0x00000000
|
|
||||||
const val SORT_ASC = 0x00000001
|
|
||||||
const val SORT_MASK = 0x00000001
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
|
||||||
const val SHOW_ALL = 0x00000000
|
|
||||||
|
|
||||||
const val SHOW_UNREAD = 0x00000002
|
|
||||||
const val SHOW_READ = 0x00000004
|
|
||||||
const val READ_MASK = 0x00000006
|
|
||||||
|
|
||||||
const val SHOW_DOWNLOADED = 0x00000008
|
|
||||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
|
||||||
const val DOWNLOADED_MASK = 0x00000018
|
|
||||||
|
|
||||||
const val SHOW_BOOKMARKED = 0x00000020
|
|
||||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
|
||||||
const val BOOKMARKED_MASK = 0x00000060
|
|
||||||
|
|
||||||
const val SORTING_SOURCE = 0x00000000
|
|
||||||
const val SORTING_NUMBER = 0x00000100
|
|
||||||
const val SORTING_MASK = 0x00000100
|
|
||||||
|
|
||||||
const val DISPLAY_NAME = 0x00000000
|
|
||||||
const val DISPLAY_NUMBER = 0x00100000
|
|
||||||
const val DISPLAY_MASK = 0x00100000
|
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
|
||||||
this.source = source
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
|
|
||||||
url = pathUrl
|
|
||||||
this.title = title
|
|
||||||
this.source = source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,29 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||||
|
|
||||||
|
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
|
||||||
|
public class MangaCategory {
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_ID, key = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_MANGA_ID)
|
||||||
|
public long manga_id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_CATEGORY_ID)
|
||||||
|
public int category_id;
|
||||||
|
|
||||||
|
public MangaCategory() {}
|
||||||
|
|
||||||
|
public static MangaCategory create(Manga manga, Category category) {
|
||||||
|
MangaCategory mc = new MangaCategory();
|
||||||
|
mc.manga_id = manga.id;
|
||||||
|
mc.category_id = category.id;
|
||||||
|
return mc;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
class MangaCategory {
|
|
||||||
|
|
||||||
var id: Long? = null
|
|
||||||
|
|
||||||
var manga_id: Long = 0
|
|
||||||
|
|
||||||
var category_id: Int = 0
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(manga: Manga, category: Category): MangaCategory {
|
|
||||||
val mc = MangaCategory()
|
|
||||||
mc.manga_id = manga.id!!
|
|
||||||
mc.category_id = category.id!!
|
|
||||||
return mc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
public class MangaChapter {
|
||||||
|
|
||||||
|
public Manga manga;
|
||||||
|
public Chapter chapter;
|
||||||
|
|
||||||
|
public MangaChapter(Manga manga, Chapter chapter) {
|
||||||
|
this.manga = manga;
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
class MangaChapter(val manga: Manga, val chapter: Chapter)
|
|
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing manga, chapter and history
|
|
||||||
*
|
|
||||||
* @param manga object containing manga
|
|
||||||
* @param chapter object containing chater
|
|
||||||
* @param history object containing history
|
|
||||||
*/
|
|
||||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
|
@ -1,49 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
open class MangaImpl : Manga {
|
|
||||||
|
|
||||||
override var id: Long? = null
|
|
||||||
|
|
||||||
override var source: Long = -1
|
|
||||||
|
|
||||||
override lateinit var url: String
|
|
||||||
|
|
||||||
override lateinit var title: String
|
|
||||||
|
|
||||||
override var artist: String? = null
|
|
||||||
|
|
||||||
override var author: String? = null
|
|
||||||
|
|
||||||
override var description: String? = null
|
|
||||||
|
|
||||||
override var genre: String? = null
|
|
||||||
|
|
||||||
override var status: Int = 0
|
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
|
||||||
|
|
||||||
override var favorite: Boolean = false
|
|
||||||
|
|
||||||
override var last_update: Long = 0
|
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
|
||||||
|
|
||||||
override var viewer: Int = 0
|
|
||||||
|
|
||||||
override var chapter_flags: Int = 0
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
|
||||||
|
|
||||||
val manga = other as Manga
|
|
||||||
|
|
||||||
return url == manga.url
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return url.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,78 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||||
|
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService;
|
||||||
|
|
||||||
|
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
||||||
|
public class MangaSync implements Serializable {
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_ID, key = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_MANGA_ID)
|
||||||
|
public long manga_id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SYNC_ID)
|
||||||
|
public int sync_id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_REMOTE_ID)
|
||||||
|
public int remote_id;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TITLE)
|
||||||
|
public String title;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_LAST_CHAPTER_READ)
|
||||||
|
public int last_chapter_read;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TOTAL_CHAPTERS)
|
||||||
|
public int total_chapters;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SCORE)
|
||||||
|
public float score;
|
||||||
|
|
||||||
|
@StorIOSQLiteColumn(name = MangaSyncTable.COL_STATUS)
|
||||||
|
public int status;
|
||||||
|
|
||||||
|
public boolean update;
|
||||||
|
|
||||||
|
public static MangaSync create() {
|
||||||
|
return new MangaSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MangaSync create(MangaSyncService service) {
|
||||||
|
MangaSync mangasync = new MangaSync();
|
||||||
|
mangasync.sync_id = service.getId();
|
||||||
|
return mangasync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyPersonalFrom(MangaSync other) {
|
||||||
|
last_chapter_read = other.last_chapter_read;
|
||||||
|
score = other.score;
|
||||||
|
status = other.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
MangaSync mangaSync = (MangaSync) o;
|
||||||
|
|
||||||
|
if (manga_id != mangaSync.manga_id) return false;
|
||||||
|
if (sync_id != mangaSync.sync_id) return false;
|
||||||
|
return remote_id == mangaSync.remote_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = (int) (manga_id ^ (manga_id >>> 32));
|
||||||
|
result = 31 * result + sync_id;
|
||||||
|
result = 31 * result + remote_id;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface Track : Serializable {
|
|
||||||
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
var manga_id: Long
|
|
||||||
|
|
||||||
var sync_id: Int
|
|
||||||
|
|
||||||
var media_id: Int
|
|
||||||
|
|
||||||
var library_id: Long?
|
|
||||||
|
|
||||||
var title: String
|
|
||||||
|
|
||||||
var last_chapter_read: Int
|
|
||||||
|
|
||||||
var total_chapters: Int
|
|
||||||
|
|
||||||
var score: Float
|
|
||||||
|
|
||||||
var status: Int
|
|
||||||
|
|
||||||
var tracking_url: String
|
|
||||||
|
|
||||||
fun copyPersonalFrom(other: Track) {
|
|
||||||
last_chapter_read = other.last_chapter_read
|
|
||||||
score = other.score
|
|
||||||
status = other.status
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(serviceId: Int): Track = TrackImpl().apply {
|
|
||||||
sync_id = serviceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
|
||||||
|
|
||||||
class TrackImpl : Track {
|
|
||||||
|
|
||||||
override var id: Long? = null
|
|
||||||
|
|
||||||
override var manga_id: Long = 0
|
|
||||||
|
|
||||||
override var sync_id: Int = 0
|
|
||||||
|
|
||||||
override var media_id: Int = 0
|
|
||||||
|
|
||||||
override var library_id: Long? = null
|
|
||||||
|
|
||||||
override lateinit var title: String
|
|
||||||
|
|
||||||
override var last_chapter_read: Int = 0
|
|
||||||
|
|
||||||
override var total_chapters: Int = 0
|
|
||||||
|
|
||||||
override var score: Float = 0f
|
|
||||||
|
|
||||||
override var status: Int = 0
|
|
||||||
|
|
||||||
override var tracking_url: String = ""
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
|
||||||
|
|
||||||
other as Track
|
|
||||||
|
|
||||||
if (manga_id != other.manga_id) return false
|
|
||||||
if (sync_id != other.sync_id) return false
|
|
||||||
return media_id == other.media_id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
|
||||||
result = 31 * result + sync_id
|
|
||||||
result = 31 * result + media_id
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,12 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
@ -34,24 +34,79 @@ interface ChapterQueries : DbProvider {
|
|||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getChapter(id: Long) = db.get()
|
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
|
||||||
|
// Add a delta to the chapter number, because binary decimal representation
|
||||||
|
// can retrieve the same chapter again
|
||||||
|
val chapterNumber = chapter.chapter_number + 0.00001
|
||||||
|
|
||||||
|
return db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
|
||||||
|
"${ChapterTable.COL_CHAPTER_NUMBER} > ? AND " +
|
||||||
|
"${ChapterTable.COL_CHAPTER_NUMBER} <= ?")
|
||||||
|
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
|
||||||
|
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
|
||||||
|
.limit(1)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNextChapterBySource(chapter: Chapter) = db.get()
|
||||||
.`object`(Chapter::class.java)
|
.`object`(Chapter::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_ID} = ?")
|
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
|
||||||
.whereArgs(id)
|
${ChapterTable.COL_SOURCE_ORDER} < ?""")
|
||||||
|
.whereArgs(chapter.manga_id, chapter.source_order)
|
||||||
|
.orderBy("${ChapterTable.COL_SOURCE_ORDER} DESC")
|
||||||
|
.limit(1)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getChapter(url: String) = db.get()
|
fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
|
||||||
|
// Add a delta to the chapter number, because binary decimal representation
|
||||||
|
// can retrieve the same chapter again
|
||||||
|
val chapterNumber = chapter.chapter_number - 0.00001
|
||||||
|
|
||||||
|
return db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder().table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
|
||||||
|
"${ChapterTable.COL_CHAPTER_NUMBER} < ? AND " +
|
||||||
|
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
|
||||||
|
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
|
||||||
|
.orderBy("${ChapterTable.COL_CHAPTER_NUMBER} DESC")
|
||||||
|
.limit(1)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPreviousChapterBySource(chapter: Chapter) = db.get()
|
||||||
.`object`(Chapter::class.java)
|
.`object`(Chapter::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
|
||||||
.whereArgs(url)
|
${ChapterTable.COL_SOURCE_ORDER} > ?""")
|
||||||
|
.whereArgs(chapter.manga_id, chapter.source_order)
|
||||||
|
.orderBy(ChapterTable.COL_SOURCE_ORDER)
|
||||||
|
.limit(1)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getNextUnreadChapter(manga: Manga) = db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
|
||||||
|
"${ChapterTable.COL_READ} = ? AND " +
|
||||||
|
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
|
||||||
|
.whereArgs(manga.id, 0, 0)
|
||||||
|
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
|
||||||
|
.limit(1)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|
||||||
@ -61,11 +116,6 @@ interface ChapterQueries : DbProvider {
|
|||||||
|
|
||||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||||
|
|
||||||
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
|
||||||
.objects(chapters)
|
|
||||||
.withPutResolver(ChapterBackupPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||||
.`object`(chapter)
|
.`object`(chapter)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface HistoryQueries : DbProvider {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert history into database
|
|
||||||
* @param history object containing history information
|
|
||||||
*/
|
|
||||||
fun insertHistory(history: History) = db.put().`object`(history).prepare()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns history of recent manga containing last read chapter
|
|
||||||
* @param date recent date range
|
|
||||||
*/
|
|
||||||
fun getRecentManga(date: Date) = db.get()
|
|
||||||
.listOfObjects(MangaChapterHistory::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(getRecentMangasQuery())
|
|
||||||
.args(date.time)
|
|
||||||
.observesTables(HistoryTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
|
||||||
.listOfObjects(History::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(getHistoryByMangaId())
|
|
||||||
.args(mangaId)
|
|
||||||
.observesTables(HistoryTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
|
||||||
.`object`(History::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(getHistoryByChapterUrl())
|
|
||||||
.args(chapterUrl)
|
|
||||||
.observesTables(HistoryTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the history last read.
|
|
||||||
* Inserts history object if not yet in database
|
|
||||||
* @param history history object
|
|
||||||
*/
|
|
||||||
fun updateHistoryLastRead(history: History) = db.put()
|
|
||||||
.`object`(history)
|
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the history last read.
|
|
||||||
* Inserts history object if not yet in database
|
|
||||||
* @param historyList history object list
|
|
||||||
*/
|
|
||||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
|
||||||
.objects(historyList)
|
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun deleteHistory() = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(HistoryTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun deleteHistoryNoLastRead() = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(HistoryTable.TABLE)
|
|
||||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
|
||||||
.whereArgs(0)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
}
|
|
@ -4,10 +4,9 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
@ -22,15 +21,15 @@ interface MangaQueries : DbProvider {
|
|||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(RawQuery.builder()
|
||||||
.query(libraryQuery)
|
.query(libraryQuery)
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
||||||
.build())
|
.build())
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
open fun getFavoriteMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
@ -40,7 +39,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Int) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
@ -67,21 +66,6 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFlagsPutResolver())
|
.withPutResolver(MangaFlagsPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateMangaFavorite(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaFavoritePutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateMangaViewer(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaViewerPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
@ -94,20 +78,4 @@ interface MangaQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun deleteMangas() = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getLastReadManga() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(getLastReadMangaQuery())
|
|
||||||
.observesTables(MangaTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
|
||||||
}
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||||
|
|
||||||
|
interface MangaSyncQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
||||||
|
.`object`(MangaSync::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
||||||
|
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
||||||
|
.whereArgs(manga.id, sync.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getMangasSync(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(MangaSync::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
|
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
}
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.database.queries
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
|
|
||||||
@ -40,67 +39,6 @@ fun getRecentsQuery() = """
|
|||||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
/**
|
|
||||||
* Query to get the recently read chapters of manga from the library up to a date.
|
|
||||||
* The max_last_read table contains the most recent chapters grouped by manga
|
|
||||||
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
|
|
||||||
* and are read after the given time period
|
|
||||||
* @return return limit is 25
|
|
||||||
*/
|
|
||||||
fun getRecentMangasQuery() = """
|
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
JOIN ${History.TABLE}
|
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
|
||||||
JOIN (
|
|
||||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
|
||||||
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
|
||||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
|
||||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
|
||||||
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
|
||||||
LIMIT 25
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun getHistoryByMangaId() = """
|
|
||||||
SELECT ${History.TABLE}.*
|
|
||||||
FROM ${History.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
|
||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun getHistoryByChapterUrl() = """
|
|
||||||
SELECT ${History.TABLE}.*
|
|
||||||
FROM ${History.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
|
||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun getLastReadMangaQuery() = """
|
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
JOIN ${History.TABLE}
|
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
|
||||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
|
||||||
ORDER BY max DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun getTotalChapterMangaQuery()= """
|
|
||||||
SELECT ${Manga.TABLE}.*
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
|
||||||
ORDER by COUNT(*)
|
|
||||||
"""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
|
||||||
|
|
||||||
interface TrackQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getTracks(manga: Manga) = db.get()
|
|
||||||
.listOfObjects(Track::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(TrackTable.TABLE)
|
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
|
||||||
|
|
||||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
|
||||||
|
|
||||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(TrackTable.TABLE)
|
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
|
||||||
.whereArgs(manga.id, sync.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|
||||||
|
|
||||||
class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(chapter)
|
|
||||||
val contentValues = mapToContentValues(chapter)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
|
||||||
.whereArgs(chapter.url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,7 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
val updateQuery = mapToUpdateQuery(chapter)
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
val contentValues = mapToContentValues(chapter)
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,9 +25,8 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
.whereArgs(chapter.id)
|
.whereArgs(chapter.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
fun mapToContentValues(chapter: Chapter) = ContentValues(2).apply {
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
val updateQuery = mapToUpdateQuery(chapter)
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
val contentValues = mapToContentValues(chapter)
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.support.annotation.NonNull
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
|
||||||
|
|
||||||
class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates last_read time of chapter
|
|
||||||
*/
|
|
||||||
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(history)
|
|
||||||
|
|
||||||
val cursor = db.lowLevel().query(Query.builder()
|
|
||||||
.table(updateQuery.table())
|
|
||||||
.where(updateQuery.where())
|
|
||||||
.whereArgs(updateQuery.whereArgs())
|
|
||||||
.build())
|
|
||||||
|
|
||||||
val putResult: PutResult
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (cursor.count == 0) {
|
|
||||||
val insertQuery = mapToInsertQuery(history)
|
|
||||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
|
||||||
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
|
|
||||||
} else {
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
|
||||||
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
putResult
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates update query
|
|
||||||
* @param obj history object
|
|
||||||
*/
|
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
|
||||||
.table(HistoryTable.TABLE)
|
|
||||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
|
||||||
.whereArgs(obj.chapter_id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create content query
|
|
||||||
* @param history object
|
|
||||||
*/
|
|
||||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
|
||||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
|
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val INSTANCE = LibraryMangaGetResolver()
|
val INSTANCE = LibraryMangaGetResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): LibraryManga {
|
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||||
val manga = LibraryManga()
|
val manga = super.mapFromCursor(cursor)
|
||||||
|
|
||||||
mapBaseFromCursor(manga, cursor)
|
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
manga.unread = cursor.getInt(unreadColumn)
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
|
||||||
|
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
|
||||||
|
manga.category = cursor.getInt(categoryColumn)
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.database.resolvers
|
|||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
||||||
|
|
||||||
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||||
|
|
||||||
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val INSTANCE = MangaChapterGetResolver()
|
val INSTANCE = MangaChapterGetResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mangaGetResolver = MangaGetResolver()
|
private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
|
||||||
|
|
||||||
private val chapterGetResolver = ChapterGetResolver()
|
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver()
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"));
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|
||||||
|
|
||||||
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
|
|
||||||
companion object {
|
|
||||||
val INSTANCE = MangaChapterHistoryGetResolver()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manga get resolver
|
|
||||||
*/
|
|
||||||
private val mangaGetResolver = MangaGetResolver()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter get resolver
|
|
||||||
*/
|
|
||||||
private val chapterResolver = ChapterGetResolver()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* History get resolver
|
|
||||||
*/
|
|
||||||
private val historyGetResolver = HistoryGetResolver()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map correct objects from cursor result
|
|
||||||
*/
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
|
|
||||||
// Get manga object
|
|
||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
|
||||||
|
|
||||||
// Get chapter object
|
|
||||||
val chapter = chapterResolver.mapFromCursor(cursor)
|
|
||||||
|
|
||||||
// Get history object
|
|
||||||
val history = historyGetResolver.mapFromCursor(cursor)
|
|
||||||
|
|
||||||
// Make certain column conflicts are dealt with
|
|
||||||
manga.id = chapter.manga_id
|
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
|
||||||
chapter.id = history.chapter_id
|
|
||||||
|
|
||||||
// Return result
|
|
||||||
return MangaChapterHistory(manga, chapter, history)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
class MangaFavoritePutResolver : PutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
|
||||||
val contentValues = mapToContentValues(manga)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
|
||||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,7 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
|
|||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
val contentValues = mapToContentValues(manga)
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
|
||||||
val contentValues = mapToContentValues(manga)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
|
||||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
class MangaViewerPutResolver : PutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
|
||||||
val contentValues = mapToContentValues(manga)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
|
||||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -14,10 +14,6 @@ object ChapterTable {
|
|||||||
|
|
||||||
const val COL_READ = "read"
|
const val COL_READ = "read"
|
||||||
|
|
||||||
const val COL_SCANLATOR = "scanlator"
|
|
||||||
|
|
||||||
const val COL_BOOKMARK = "bookmark"
|
|
||||||
|
|
||||||
const val COL_DATE_FETCH = "date_fetch"
|
const val COL_DATE_FETCH = "date_fetch"
|
||||||
|
|
||||||
const val COL_DATE_UPLOAD = "date_upload"
|
const val COL_DATE_UPLOAD = "date_upload"
|
||||||
@ -34,9 +30,7 @@ object ChapterTable {
|
|||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_URL TEXT NOT NULL,
|
$COL_URL TEXT NOT NULL,
|
||||||
$COL_NAME TEXT NOT NULL,
|
$COL_NAME TEXT NOT NULL,
|
||||||
$COL_SCANLATOR TEXT,
|
|
||||||
$COL_READ BOOLEAN NOT NULL,
|
$COL_READ BOOLEAN NOT NULL,
|
||||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||||
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
||||||
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
||||||
@ -52,10 +46,4 @@ object ChapterTable {
|
|||||||
val sourceOrderUpdateQuery: String
|
val sourceOrderUpdateQuery: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
||||||
|
|
||||||
val bookmarkUpdateQuery: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
|
||||||
|
|
||||||
val addScanlator: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object HistoryTable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table name
|
|
||||||
*/
|
|
||||||
const val TABLE = "history"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id column name
|
|
||||||
*/
|
|
||||||
const val COL_ID = "${TABLE}_id"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter id column name
|
|
||||||
*/
|
|
||||||
const val COL_CHAPTER_ID = "${TABLE}_chapter_id"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last read column name
|
|
||||||
*/
|
|
||||||
const val COL_LAST_READ = "${TABLE}_last_read"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time read column name
|
|
||||||
*/
|
|
||||||
const val COL_TIME_READ = "${TABLE}_time_read"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* query to create history table
|
|
||||||
*/
|
|
||||||
val createTableQuery: String
|
|
||||||
get() = """CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
|
||||||
$COL_LAST_READ LONG,
|
|
||||||
$COL_TIME_READ LONG,
|
|
||||||
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* query to index history chapter id
|
|
||||||
*/
|
|
||||||
val createChapterIdIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
object TrackTable {
|
object MangaSyncTable {
|
||||||
|
|
||||||
const val TABLE = "manga_sync"
|
const val TABLE = "manga_sync"
|
||||||
|
|
||||||
@ -10,9 +10,7 @@ object TrackTable {
|
|||||||
|
|
||||||
const val COL_SYNC_ID = "sync_id"
|
const val COL_SYNC_ID = "sync_id"
|
||||||
|
|
||||||
const val COL_MEDIA_ID = "remote_id"
|
const val COL_REMOTE_ID = "remote_id"
|
||||||
|
|
||||||
const val COL_LIBRARY_ID = "library_id"
|
|
||||||
|
|
||||||
const val COL_TITLE = "title"
|
const val COL_TITLE = "title"
|
||||||
|
|
||||||
@ -24,29 +22,20 @@ object TrackTable {
|
|||||||
|
|
||||||
const val COL_TOTAL_CHAPTERS = "total_chapters"
|
const val COL_TOTAL_CHAPTERS = "total_chapters"
|
||||||
|
|
||||||
const val COL_TRACKING_URL = "remote_url"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() = """CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_SYNC_ID INTEGER NOT NULL,
|
$COL_SYNC_ID INTEGER NOT NULL,
|
||||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
$COL_REMOTE_ID INTEGER NOT NULL,
|
||||||
$COL_LIBRARY_ID INTEGER,
|
|
||||||
$COL_TITLE TEXT NOT NULL,
|
$COL_TITLE TEXT NOT NULL,
|
||||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
$COL_STATUS INTEGER NOT NULL,
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
$COL_SCORE FLOAT NOT NULL,
|
||||||
$COL_TRACKING_URL TEXT NOT NULL,
|
|
||||||
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val addTrackingUrl: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
|
|
||||||
|
|
||||||
val addLibraryId: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
|
|
||||||
}
|
}
|
@ -1,272 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
|
||||||
* directory checking is expensive and it slowdowns the app. The cache is invalidated by the time
|
|
||||||
* defined in [renewInterval] as we don't have any control over the filesystem and the user can
|
|
||||||
* delete the folders at any time without the app noticing.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
* @param provider the downloads directories provider.
|
|
||||||
* @param sourceManager the source manager.
|
|
||||||
* @param preferences the preferences of the app.
|
|
||||||
*/
|
|
||||||
class DownloadCache(
|
|
||||||
private val context: Context,
|
|
||||||
private val provider: DownloadProvider,
|
|
||||||
private val sourceManager: SourceManager,
|
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
|
|
||||||
* issues, as the cache is only used for UI feedback.
|
|
||||||
*/
|
|
||||||
private val renewInterval = TimeUnit.HOURS.toMillis(1)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last time the cache was refreshed.
|
|
||||||
*/
|
|
||||||
private var lastRenew = 0L
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The root directory for downloads.
|
|
||||||
*/
|
|
||||||
private var rootDir = RootDirectory(getDirectoryFromPreference())
|
|
||||||
|
|
||||||
init {
|
|
||||||
preferences.downloadsDirectory().asObservable()
|
|
||||||
.skip(1)
|
|
||||||
.subscribe {
|
|
||||||
lastRenew = 0L // invalidate cache
|
|
||||||
rootDir = RootDirectory(getDirectoryFromPreference())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the downloads directory from the user's preferences.
|
|
||||||
*/
|
|
||||||
private fun getDirectoryFromPreference(): UniFile {
|
|
||||||
val dir = preferences.downloadsDirectory().getOrDefault()
|
|
||||||
return UniFile.fromUri(context, Uri.parse(dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the chapter is downloaded.
|
|
||||||
*
|
|
||||||
* @param chapter the chapter to check.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
|
||||||
*/
|
|
||||||
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
|
|
||||||
if (skipCache) {
|
|
||||||
val source = sourceManager.get(manga.source) ?: return false
|
|
||||||
return provider.findChapterDir(chapter, manga, source) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
checkRenew()
|
|
||||||
|
|
||||||
val sourceDir = rootDir.files[manga.source]
|
|
||||||
if (sourceDir != null) {
|
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
|
||||||
if (mangaDir != null) {
|
|
||||||
return provider.getChapterDirName(chapter) in mangaDir.files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the amount of downloaded chapters for a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to check.
|
|
||||||
*/
|
|
||||||
fun getDownloadCount(manga: Manga): Int {
|
|
||||||
checkRenew()
|
|
||||||
|
|
||||||
val sourceDir = rootDir.files[manga.source]
|
|
||||||
if (sourceDir != null) {
|
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
|
||||||
if (mangaDir != null) {
|
|
||||||
return mangaDir.files.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the cache needs a renewal and performs it if needed.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
private fun checkRenew() {
|
|
||||||
if (lastRenew + renewInterval < System.currentTimeMillis()) {
|
|
||||||
renew()
|
|
||||||
lastRenew = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renews the downloads cache.
|
|
||||||
*/
|
|
||||||
private fun renew() {
|
|
||||||
val onlineSources = sourceManager.getOnlineSources()
|
|
||||||
|
|
||||||
val sourceDirs = rootDir.dir.listFiles()
|
|
||||||
.orEmpty()
|
|
||||||
.associate { it.name to SourceDirectory(it) }
|
|
||||||
.mapNotNullKeys { entry ->
|
|
||||||
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
rootDir.files = sourceDirs
|
|
||||||
|
|
||||||
sourceDirs.values.forEach { sourceDir ->
|
|
||||||
val mangaDirs = sourceDir.dir.listFiles()
|
|
||||||
.orEmpty()
|
|
||||||
.associateNotNullKeys { it.name to MangaDirectory(it) }
|
|
||||||
|
|
||||||
sourceDir.files = mangaDirs
|
|
||||||
|
|
||||||
mangaDirs.values.forEach { mangaDir ->
|
|
||||||
val chapterDirs = mangaDir.dir.listFiles()
|
|
||||||
.orEmpty()
|
|
||||||
.mapNotNull { it.name }
|
|
||||||
.toHashSet()
|
|
||||||
|
|
||||||
mangaDir.files = chapterDirs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a chapter that has just been download to this cache.
|
|
||||||
*
|
|
||||||
* @param chapterDirName the downloaded chapter's directory name.
|
|
||||||
* @param mangaUniFile the directory of the manga.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
|
|
||||||
// Retrieve the cached source directory or cache a new one
|
|
||||||
var sourceDir = rootDir.files[manga.source]
|
|
||||||
if (sourceDir == null) {
|
|
||||||
val source = sourceManager.get(manga.source) ?: return
|
|
||||||
val sourceUniFile = provider.findSourceDir(source) ?: return
|
|
||||||
sourceDir = SourceDirectory(sourceUniFile)
|
|
||||||
rootDir.files += manga.source to sourceDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the cached manga directory or cache a new one
|
|
||||||
val mangaDirName = provider.getMangaDirName(manga)
|
|
||||||
var mangaDir = sourceDir.files[mangaDirName]
|
|
||||||
if (mangaDir == null) {
|
|
||||||
mangaDir = MangaDirectory(mangaUniFile)
|
|
||||||
sourceDir.files += mangaDirName to mangaDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the chapter directory
|
|
||||||
mangaDir.files += chapterDirName
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a chapter that has been deleted from this cache.
|
|
||||||
*
|
|
||||||
* @param chapter the chapter to remove.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun removeChapter(chapter: Chapter, manga: Manga) {
|
|
||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
|
||||||
val chapterDirName = provider.getChapterDirName(chapter)
|
|
||||||
if (chapterDirName in mangaDir.files) {
|
|
||||||
mangaDir.files -= chapterDirName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a list of chapters that have been deleted from this cache.
|
|
||||||
*
|
|
||||||
* @param chapters the list of chapter to remove.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
|
||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
|
||||||
for (chapter in chapters) {
|
|
||||||
val chapterDirName = provider.getChapterDirName(chapter)
|
|
||||||
if (chapterDirName in mangaDir.files) {
|
|
||||||
mangaDir.files -= chapterDirName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a manga that has been deleted from this cache.
|
|
||||||
*
|
|
||||||
* @param manga the manga to remove.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun removeManga(manga: Manga) {
|
|
||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
|
||||||
val mangaDirName = provider.getMangaDirName(manga)
|
|
||||||
if (mangaDirName in sourceDir.files) {
|
|
||||||
sourceDir.files -= mangaDirName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to store the files under the root downloads directory.
|
|
||||||
*/
|
|
||||||
private class RootDirectory(val dir: UniFile,
|
|
||||||
var files: Map<Long, SourceDirectory> = hashMapOf())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to store the files under a source directory.
|
|
||||||
*/
|
|
||||||
private class SourceDirectory(val dir: UniFile,
|
|
||||||
var files: Map<String, MangaDirectory> = hashMapOf())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to store the files under a manga directory.
|
|
||||||
*/
|
|
||||||
private class MangaDirectory(val dir: UniFile,
|
|
||||||
var files: Set<String> = hashSetOf())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
|
||||||
*/
|
|
||||||
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
|
|
||||||
val destination = LinkedHashMap<R, V>()
|
|
||||||
forEach { element -> transform(element)?.let { destination.put(it, element.value) } }
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a map from a list containing only the key entries of [transform] that are not null.
|
|
||||||
*/
|
|
||||||
private inline fun <T, K, V> Array<T>.associateNotNullKeys(transform: (T) -> Pair<K?, V>): Map<K, V> {
|
|
||||||
val destination = LinkedHashMap<K, V>()
|
|
||||||
for (element in this) {
|
|
||||||
val (key, value) = transform(element)
|
|
||||||
if (key != null) {
|
|
||||||
destination.put(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,210 +1,449 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.hippo.unifile.UniFile
|
import android.net.Uri
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import eu.kanade.tachiyomi.util.DiskUtils
|
||||||
|
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
|
||||||
|
import eu.kanade.tachiyomi.util.UrlUtil
|
||||||
|
import eu.kanade.tachiyomi.util.saveImageTo
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import rx.subjects.BehaviorSubject
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
|
||||||
* This class is used to manage chapter downloads in the application. It must be instantiated once
|
|
||||||
* and retrieved through dependency injection. You can use this class to queue new chapters or query
|
|
||||||
* downloaded chapters.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
class DownloadManager(context: Context) {
|
|
||||||
|
|
||||||
/**
|
private val gson = Gson()
|
||||||
* The sources manager.
|
|
||||||
*/
|
|
||||||
private val sourceManager by injectLazy<SourceManager>()
|
|
||||||
|
|
||||||
/**
|
private val downloadsQueueSubject = PublishSubject.create<List<Download>>()
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
val runningSubject = BehaviorSubject.create<Boolean>()
|
||||||
*/
|
private var downloadsSubscription: Subscription? = null
|
||||||
private val provider = DownloadProvider(context)
|
|
||||||
|
|
||||||
/**
|
val downloadNotifier by lazy { DownloadNotifier(context) }
|
||||||
* Cache of downloaded chapters.
|
|
||||||
*/
|
|
||||||
private val cache = DownloadCache(context, provider, sourceManager)
|
|
||||||
|
|
||||||
/**
|
private val threadsSubject = BehaviorSubject.create<Int>()
|
||||||
* Downloader whose only task is to download chapters.
|
private var threadsSubscription: Subscription? = null
|
||||||
*/
|
|
||||||
private val downloader = Downloader(context, provider, cache, sourceManager)
|
|
||||||
|
|
||||||
/**
|
val queue = DownloadQueue()
|
||||||
* Queue to delay the deletion of a list of chapters until triggered.
|
|
||||||
*/
|
|
||||||
private val pendingDeleter = DownloadPendingDeleter(context)
|
|
||||||
|
|
||||||
/**
|
val imageFilenameRegex = "[^\\sa-zA-Z0-9.-]".toRegex()
|
||||||
* Downloads queue, where the pending chapters are stored.
|
|
||||||
*/
|
|
||||||
val queue: DownloadQueue
|
|
||||||
get() = downloader.queue
|
|
||||||
|
|
||||||
/**
|
val PAGE_LIST_FILE = "index.json"
|
||||||
* Subject for subscribing to downloader status.
|
|
||||||
*/
|
|
||||||
val runningRelay: BehaviorRelay<Boolean>
|
|
||||||
get() = downloader.runningRelay
|
|
||||||
|
|
||||||
/**
|
@Volatile var isRunning: Boolean = false
|
||||||
* Tells the downloader to begin downloads.
|
private set
|
||||||
*
|
|
||||||
* @return true if it's started, false otherwise (empty queue).
|
|
||||||
*/
|
|
||||||
fun startDownloads(): Boolean {
|
|
||||||
return downloader.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private fun initializeSubscriptions() {
|
||||||
* Tells the downloader to stop downloads.
|
|
||||||
*
|
|
||||||
* @param reason an optional reason for being stopped, used to notify the user.
|
|
||||||
*/
|
|
||||||
fun stopDownloads(reason: String? = null) {
|
|
||||||
downloader.stop(reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
downloadsSubscription?.unsubscribe()
|
||||||
* Tells the downloader to pause downloads.
|
|
||||||
*/
|
|
||||||
fun pauseDownloads() {
|
|
||||||
downloader.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
threadsSubscription = preferences.downloadThreads().asObservable()
|
||||||
* Empties the download queue.
|
.subscribe {
|
||||||
*
|
threadsSubject.onNext(it)
|
||||||
* @param isNotification value that determines if status is set (needed for view updates)
|
downloadNotifier.multipleDownloadThreads = it > 1
|
||||||
*/
|
}
|
||||||
fun clearQueue(isNotification: Boolean = false) {
|
|
||||||
downloader.clearQueue(isNotification)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
|
||||||
* Tells the downloader to enqueue the given list of chapters.
|
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
||||||
*
|
.onBackpressureBuffer()
|
||||||
* @param manga the manga of the chapters.
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
* @param chapters the list of chapters to enqueue.
|
.subscribe({
|
||||||
* @param autoStart whether to start the downloader after enqueing the chapters.
|
// Delete successful downloads from queue
|
||||||
*/
|
if (it.status == Download.DOWNLOADED) {
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
|
// remove downloaded chapter from queue
|
||||||
downloader.queueChapters(manga, chapters, autoStart)
|
queue.del(it)
|
||||||
}
|
downloadNotifier.onProgressChange(queue)
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the page list of a downloaded chapter.
|
|
||||||
*
|
|
||||||
* @param source the source of the chapter.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
* @param chapter the downloaded chapter.
|
|
||||||
* @return an observable containing the list of pages from the chapter.
|
|
||||||
*/
|
|
||||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
|
|
||||||
return buildPageList(provider.findChapterDir(chapter, manga, source))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the page list of a downloaded chapter.
|
|
||||||
*
|
|
||||||
* @param chapterDir the file where the chapter is downloaded.
|
|
||||||
* @return an observable containing the list of pages from the chapter.
|
|
||||||
*/
|
|
||||||
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
|
|
||||||
return Observable.fromCallable {
|
|
||||||
val files = chapterDir?.listFiles().orEmpty()
|
|
||||||
.filter { "image" in it.type.orEmpty() }
|
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
|
||||||
throw Exception("Page list is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
files.sortedBy { it.name }
|
|
||||||
.mapIndexed { i, file ->
|
|
||||||
Page(i, uri = file.uri).apply { status = Page.READY }
|
|
||||||
}
|
}
|
||||||
|
if (areAllDownloadsFinished()) {
|
||||||
|
DownloadService.stop(context)
|
||||||
|
}
|
||||||
|
}, { e ->
|
||||||
|
DownloadService.stop(context)
|
||||||
|
Timber.e(e, e.message)
|
||||||
|
downloadNotifier.onError(e.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isRunning) {
|
||||||
|
isRunning = true
|
||||||
|
runningSubject.onNext(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun destroySubscriptions() {
|
||||||
* Returns true if the chapter is downloaded.
|
if (isRunning) {
|
||||||
*
|
isRunning = false
|
||||||
* @param chapter the chapter to check.
|
runningSubject.onNext(false)
|
||||||
* @param manga the manga of the chapter.
|
}
|
||||||
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
|
||||||
*/
|
if (downloadsSubscription != null) {
|
||||||
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean = false): Boolean {
|
downloadsSubscription?.unsubscribe()
|
||||||
return cache.isChapterDownloaded(chapter, manga, skipCache)
|
downloadsSubscription = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadsSubscription != null) {
|
||||||
|
threadsSubscription?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Create a download object for every chapter and add them to the downloads queue
|
||||||
* Returns the amount of downloaded chapters for a manga.
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
*
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
|
||||||
* @param manga the manga to check.
|
|
||||||
*/
|
// Add chapters to queue from the start
|
||||||
fun getDownloadCount(manga: Manga): Int {
|
val sortedChapters = chapters.sortedByDescending { it.source_order }
|
||||||
return cache.getDownloadCount(manga)
|
|
||||||
|
// Used to avoid downloading chapters with the same name
|
||||||
|
val addedChapters = ArrayList<String>()
|
||||||
|
val pending = ArrayList<Download>()
|
||||||
|
|
||||||
|
for (chapter in sortedChapters) {
|
||||||
|
if (addedChapters.contains(chapter.name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
addedChapters.add(chapter.name)
|
||||||
|
val download = Download(source, manga, chapter)
|
||||||
|
|
||||||
|
if (!prepareDownload(download)) {
|
||||||
|
queue.add(download)
|
||||||
|
pending.add(download)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize queue size
|
||||||
|
downloadNotifier.initialQueueSize = queue.size
|
||||||
|
// Show notification
|
||||||
|
downloadNotifier.onProgressChange(queue)
|
||||||
|
|
||||||
|
if (isRunning) downloadsQueueSubject.onNext(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Public method to check if a chapter is downloaded
|
||||||
* Deletes the directories of a list of downloaded chapters.
|
fun isChapterDownloaded(source: Source, manga: Manga, chapter: Chapter): Boolean {
|
||||||
*
|
val directory = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||||
* @param chapters the list of chapters to delete.
|
if (!directory.exists())
|
||||||
* @param manga the manga of the chapters.
|
return false
|
||||||
* @param source the source of the chapters.
|
|
||||||
*/
|
val pages = getSavedPageList(source, manga, chapter)
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
return isChapterDownloaded(directory, pages)
|
||||||
queue.remove(chapters)
|
}
|
||||||
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
|
|
||||||
chapterDirs.forEach { it.delete() }
|
// Prepare the download. Returns true if the chapter is already downloaded
|
||||||
cache.removeChapters(chapters, manga)
|
private fun prepareDownload(download: Download): Boolean {
|
||||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
// If the chapter is already queued, don't add it again
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
for (queuedDownload in queue) {
|
||||||
|
if (download.chapter.id == queuedDownload.chapter.id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the directory to the download object for future access
|
||||||
|
download.directory = getAbsoluteChapterDirectory(download)
|
||||||
|
|
||||||
|
// If the directory doesn't exist, the chapter isn't downloaded.
|
||||||
|
if (!download.directory.exists()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the page list doesn't exist, the chapter isn't downloaded
|
||||||
|
val savedPages = getSavedPageList(download) ?: return false
|
||||||
|
|
||||||
|
// Add the page list to the download object for future access
|
||||||
|
download.pages = savedPages
|
||||||
|
|
||||||
|
// If the number of files matches the number of pages, the chapter is downloaded.
|
||||||
|
// We have the index file, so we check one file more
|
||||||
|
return isChapterDownloaded(download.directory, download.pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all the images are downloaded
|
||||||
|
private fun isChapterDownloaded(directory: File, pages: List<Page>?): Boolean {
|
||||||
|
return pages != null && !pages.isEmpty() && pages.size + 1 == directory.listFiles().size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the entire chapter
|
||||||
|
private fun downloadChapter(download: Download): Observable<Download> {
|
||||||
|
DiskUtils.createDirectory(download.directory)
|
||||||
|
|
||||||
|
val pageListObservable = if (download.pages == null)
|
||||||
|
// Pull page list from network and add them to download object
|
||||||
|
download.source.fetchPageListFromNetwork(download.chapter)
|
||||||
|
.doOnNext { pages ->
|
||||||
|
download.pages = pages
|
||||||
|
savePageList(download)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// Or if the page list already exists, start from the file
|
||||||
|
Observable.just(download.pages)
|
||||||
|
|
||||||
|
return Observable.defer {
|
||||||
|
pageListObservable
|
||||||
|
.doOnNext { pages ->
|
||||||
|
download.downloadedImages = 0
|
||||||
|
download.status = Download.DOWNLOADING
|
||||||
|
}
|
||||||
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
|
// Start downloading images, consider we can have downloaded images already
|
||||||
|
.concatMap { page -> getOrDownloadImage(page, download) }
|
||||||
|
// Do when page is downloaded.
|
||||||
|
.doOnNext {
|
||||||
|
downloadNotifier.onProgressChange(download, queue)
|
||||||
|
}
|
||||||
|
// Do after download completes
|
||||||
|
.doOnCompleted { onDownloadCompleted(download) }
|
||||||
|
.toList()
|
||||||
|
.map { pages -> download }
|
||||||
|
// If the page list threw, it will resume here
|
||||||
|
.onErrorResumeNext { error ->
|
||||||
|
download.status = Download.ERROR
|
||||||
|
downloadNotifier.onError(error.message, download.chapter.name)
|
||||||
|
Observable.just(download)
|
||||||
|
}
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the image from the filesystem if it exists or download from network
|
||||||
|
private fun getOrDownloadImage(page: Page, download: Download): Observable<Page> {
|
||||||
|
// If the image URL is empty, do nothing
|
||||||
|
if (page.imageUrl == null)
|
||||||
|
return Observable.just(page)
|
||||||
|
|
||||||
|
val filename = getImageFilename(page)
|
||||||
|
val imagePath = File(download.directory, filename)
|
||||||
|
|
||||||
|
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||||
|
val pageObservable = if (isImageDownloaded(imagePath))
|
||||||
|
Observable.just(page)
|
||||||
|
else
|
||||||
|
downloadImage(page, download.source, download.directory, filename)
|
||||||
|
|
||||||
|
return pageObservable
|
||||||
|
// When the image is ready, set image path, progress (just in case) and status
|
||||||
|
.doOnNext {
|
||||||
|
page.imagePath = imagePath.absolutePath
|
||||||
|
page.progress = 100
|
||||||
|
download.downloadedImages++
|
||||||
|
page.status = Page.READY
|
||||||
|
}
|
||||||
|
// Mark this page as error and allow to download the remaining
|
||||||
|
.onErrorResumeNext {
|
||||||
|
page.progress = 0
|
||||||
|
page.status = Page.ERROR
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save image on disk
|
||||||
|
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
|
||||||
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
|
return source.imageResponse(page)
|
||||||
|
.flatMap {
|
||||||
|
try {
|
||||||
|
val file = File(directory, filename)
|
||||||
|
file.parentFile.mkdirs()
|
||||||
|
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
it.body().close()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
.retryWhen {
|
||||||
|
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
|
||||||
|
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
||||||
|
fun getDownloadedImage(page: Page, chapterDir: File): Observable<Page> {
|
||||||
|
if (page.imageUrl == null) {
|
||||||
|
page.status = Page.ERROR
|
||||||
|
return Observable.just(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imagePath = File(chapterDir, getImageFilename(page))
|
||||||
|
|
||||||
|
// When the image is ready, set image path, progress (just in case) and status
|
||||||
|
if (isImageDownloaded(imagePath)) {
|
||||||
|
page.imagePath = imagePath.absolutePath
|
||||||
|
page.progress = 100
|
||||||
|
page.status = Page.READY
|
||||||
|
} else {
|
||||||
|
page.status = Page.ERROR
|
||||||
|
}
|
||||||
|
return Observable.just(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the filename for an image given the page
|
||||||
|
private fun getImageFilename(page: Page): String {
|
||||||
|
val url = page.imageUrl
|
||||||
|
val number = String.format("%03d", page.pageNumber + 1)
|
||||||
|
|
||||||
|
// Try to preserve file extension
|
||||||
|
return when {
|
||||||
|
UrlUtil.isJpg(url) -> "$number.jpg"
|
||||||
|
UrlUtil.isPng(url) -> "$number.png"
|
||||||
|
UrlUtil.isGif(url) -> "$number.gif"
|
||||||
|
else -> Uri.parse(url).lastPathSegment.replace(imageFilenameRegex, "_")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun isImageDownloaded(imagePath: File): Boolean {
|
||||||
* Deletes the directory of a downloaded manga.
|
return imagePath.exists()
|
||||||
*
|
|
||||||
* @param manga the manga to delete.
|
|
||||||
* @param source the source of the manga.
|
|
||||||
*/
|
|
||||||
fun deleteManga(manga: Manga, source: Source) {
|
|
||||||
queue.remove(manga)
|
|
||||||
provider.findMangaDir(manga, source)?.delete()
|
|
||||||
cache.removeManga(manga)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Called when a download finishes. This doesn't mean the download was successful, so we check it
|
||||||
* Adds a list of chapters to be deleted later.
|
private fun onDownloadCompleted(download: Download) {
|
||||||
*
|
checkDownloadIsSuccessful(download)
|
||||||
* @param chapters the list of chapters to delete.
|
savePageList(download)
|
||||||
* @param manga the manga of the chapters.
|
|
||||||
*/
|
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
|
||||||
pendingDeleter.addChapters(chapters, manga)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun checkDownloadIsSuccessful(download: Download) {
|
||||||
* Triggers the execution of the deletion of pending chapters.
|
var actualProgress = 0
|
||||||
*/
|
var status = Download.DOWNLOADED
|
||||||
fun deletePendingChapters() {
|
// If any page has an error, the download result will be error
|
||||||
val pendingChapters = pendingDeleter.getPendingChapters()
|
for (page in download.pages) {
|
||||||
for ((manga, chapters) in pendingChapters) {
|
actualProgress += page.progress
|
||||||
val source = sourceManager.get(manga.source) ?: continue
|
if (page.status != Page.READY) {
|
||||||
deleteChapters(chapters, manga, source)
|
status = Download.ERROR
|
||||||
|
downloadNotifier.onError(context.getString(R.string.download_notifier_page_ready_error), download.chapter.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Ensure that the chapter folder has all the images
|
||||||
|
if (!isChapterDownloaded(download.directory, download.pages)) {
|
||||||
|
status = Download.ERROR
|
||||||
|
downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name)
|
||||||
|
}
|
||||||
|
download.totalProgress = actualProgress
|
||||||
|
download.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the page list from the chapter's directory if it exists, null otherwise
|
||||||
|
fun getSavedPageList(source: Source, manga: Manga, chapter: Chapter): List<Page>? {
|
||||||
|
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||||
|
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
JsonReader(FileReader(pagesFile)).use {
|
||||||
|
val collectionType = object : TypeToken<List<Page>>() {}.type
|
||||||
|
gson.fromJson(it, collectionType)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcut for the method above
|
||||||
|
private fun getSavedPageList(download: Download): List<Page>? {
|
||||||
|
return getSavedPageList(download.source, download.manga, download.chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the page list to the chapter's directory
|
||||||
|
fun savePageList(source: Source, manga: Manga, chapter: Chapter, pages: List<Page>) {
|
||||||
|
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||||
|
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
||||||
|
|
||||||
|
pagesFile.outputStream().use {
|
||||||
|
try {
|
||||||
|
it.write(gson.toJson(pages).toByteArray())
|
||||||
|
it.flush()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcut for the method above
|
||||||
|
private fun savePageList(download: Download) {
|
||||||
|
savePageList(download.source, download.manga, download.chapter, download.pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
||||||
|
val mangaRelativePath = source.toString() +
|
||||||
|
File.separator +
|
||||||
|
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||||
|
|
||||||
|
return File(preferences.downloadsDirectory().getOrDefault(), mangaRelativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the absolute path to the chapter directory
|
||||||
|
fun getAbsoluteChapterDirectory(source: Source, manga: Manga, chapter: Chapter): File {
|
||||||
|
val chapterRelativePath = chapter.name.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||||
|
|
||||||
|
return File(getAbsoluteMangaDirectory(source, manga), chapterRelativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcut for the method above
|
||||||
|
private fun getAbsoluteChapterDirectory(download: Download): File {
|
||||||
|
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
|
||||||
|
val path = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||||
|
DiskUtils.deleteFiles(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun areAllDownloadsFinished(): Boolean {
|
||||||
|
for (download in queue) {
|
||||||
|
if (download.status <= Download.DOWNLOADING)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDownloads(): Boolean {
|
||||||
|
if (queue.isEmpty())
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (downloadsSubscription == null || downloadsSubscription!!.isUnsubscribed)
|
||||||
|
initializeSubscriptions()
|
||||||
|
|
||||||
|
val pending = ArrayList<Download>()
|
||||||
|
for (download in queue) {
|
||||||
|
if (download.status != Download.DOWNLOADED) {
|
||||||
|
if (download.status != Download.QUEUE) download.status = Download.QUEUE
|
||||||
|
pending.add(download)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadsQueueSubject.onNext(pending)
|
||||||
|
|
||||||
|
return !pending.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopDownloads(errorMessage: String? = null) {
|
||||||
|
destroySubscriptions()
|
||||||
|
for (download in queue) {
|
||||||
|
if (download.status == Download.DOWNLOADING) {
|
||||||
|
download.status = Download.ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorMessage?.let { downloadNotifier.onError(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearQueue() {
|
||||||
|
queue.clear()
|
||||||
|
downloadNotifier.onClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|