Compare commits
407 Commits
Author | SHA1 | Date | |
---|---|---|---|
82141cec6e | |||
218313428f | |||
44b47b49bc | |||
2f69317f5d | |||
e1eff7b744 | |||
3aa12281c3 | |||
72920130c0 | |||
0fd00331e1 | |||
7a4763ee68 | |||
647a78b791 | |||
82faa91ce3 | |||
ad664dfb9f | |||
005ac9e732 | |||
d8e3fe542d | |||
c40e4f6c5a | |||
9bb3195bca | |||
eee0bc4985 | |||
a24d670f54 | |||
51e049ab78 | |||
01e37dfab8 | |||
74087edebb | |||
db58c9b77f | |||
c4dad1c20b | |||
cae04656b9 | |||
aa57b1bc77 | |||
491d476cac | |||
f0053a2f78 | |||
10e7a3b35b | |||
4147fd6b19 | |||
2bb903088e | |||
c90f985fcc | |||
2ebaacfc89 | |||
c339bd49d0 | |||
c349fb0e37 | |||
bc825bdefa | |||
ed49ce8e1d | |||
ad2ecd538d | |||
ff8e3f0af4 | |||
698e17178a | |||
ebeee70931 | |||
b8b118bdeb | |||
5ddd7d1b14 | |||
450b23436f | |||
89793ac338 | |||
c456812a46 | |||
6f8f6e9233 | |||
5770d00f81 | |||
a0fb1eff4a | |||
89dc240a22 | |||
ee4f069341 | |||
011bb9f5b1 | |||
08b06e1b4e | |||
0416a2ff15 | |||
3a7cdfcaa4 | |||
c36a47576d | |||
6c9135c093 | |||
80ea9001b3 | |||
24bb94ceac | |||
0f16351f5f | |||
b60b26bbd0 | |||
86e53e08de | |||
d3cb10a74e | |||
7d6cfff719 | |||
cc0fe0a1a9 | |||
25327342fb | |||
e02cf67f85 | |||
bf4bef6d62 | |||
76645bce6e | |||
9276c491bc | |||
fa59b4f8a7 | |||
934a37c36b | |||
5362f62078 | |||
ccd360687e | |||
5a2e8a838c | |||
3abae1cc75 | |||
b68ef8c983 | |||
d5f5ba95bb | |||
e8638cb0b3 | |||
62f9071adc | |||
1d079dd9a4 | |||
cccb56bda1 | |||
5d8dc241d8 | |||
9ba7312caf | |||
8ebda219c4 | |||
47f14e8555 | |||
974a24d03b | |||
15f225537e | |||
a32572fc96 | |||
be3ed9b6af | |||
a0939e1c48 | |||
003dca9d45 | |||
5c1770247c | |||
021dde66eb | |||
5840a3e1e2 | |||
7c6478fe6b | |||
68aca55e6f | |||
ba674935f4 | |||
a053d55fbc | |||
38ba8852a3 | |||
3533359fae | |||
0a988d1c69 | |||
5f9e65cc9b | |||
026188268d | |||
0e3464457c | |||
56195434e7 | |||
ba2194f435 | |||
e7df172da1 | |||
e7606e6dca | |||
8d4c0f505c | |||
8f2878a841 | |||
77296348a0 | |||
a62a7d5330 | |||
bf60aae9d8 | |||
ecc1520100 | |||
f1f6a2b341 | |||
55bf1c31a6 | |||
e47dd3d587 | |||
af0e3a278f | |||
493ad93957 | |||
dbe8f3cfbe | |||
08cdac968d | |||
f12d5ba689 | |||
0afd77d110 | |||
7551941ef2 | |||
9ca0307e1c | |||
9a6f8be28c | |||
9baf3b5a09 | |||
ca3f0873f3 | |||
adb0201449 | |||
cf293642fb | |||
10e1106760 | |||
3f2d375a53 | |||
f8e121ee06 | |||
0ee005579b | |||
6ecd7fced8 | |||
aeaf4d78f8 | |||
7baf0ddcc2 | |||
d79e141fe5 | |||
030071e659 | |||
9cbf226cfd | |||
36aabf23e1 | |||
8b67255186 | |||
3186661420 | |||
46896d9e86 | |||
2c4fd340c8 | |||
ae6d052978 | |||
974891a085 | |||
d44cd16682 | |||
23e99a3ed8 | |||
024a457250 | |||
788cb843fc | |||
790e0908a3 | |||
7a45cd5b56 | |||
f61a8ce51d | |||
fcce29a467 | |||
5f568733f3 | |||
96340de17d | |||
3611f67fb4 | |||
353ccbd444 | |||
3c1179d27b | |||
e502caee9f | |||
da8b870670 | |||
6b26859983 | |||
7afd224aff | |||
62e7bead73 | |||
116f7d1c4a | |||
18f89cc341 | |||
7c99ae1b3b | |||
16dc4d298d | |||
762c378bd6 | |||
515289134e | |||
3d1afe7cf2 | |||
fd825b1049 | |||
136e90638a | |||
9bf071132d | |||
014bb2f426 | |||
56927927c8 | |||
b19a4d2977 | |||
f4b838d8e2 | |||
c6cfd24f19 | |||
10f36f40d6 | |||
9d5cf9163a | |||
9abce0cca3 | |||
c6245f4fa3 | |||
75fc160204 | |||
263198dd89 | |||
345f96055d | |||
51144aa45e | |||
86a599d13f | |||
0cf81e6f7a | |||
8874fe973c | |||
f8a03226ee | |||
32db1e3045 | |||
303e6c0102 | |||
18883f1ba3 | |||
5c31271e91 | |||
00981cf4e8 | |||
968f4a69e8 | |||
e7e1a9bf50 | |||
fe1becb001 | |||
7789171c71 | |||
3fd2222c99 | |||
6de36a88c0 | |||
b37685542d | |||
a2b1b9e746 | |||
8017324033 | |||
7464497c88 | |||
499def3daa | |||
6931b75cc5 | |||
f853610578 | |||
69f51b88bf | |||
e0d680201a | |||
1566b8f8b8 | |||
4bbf78e840 | |||
7ab16a69df | |||
95e60ed775 | |||
d38cd2547a | |||
2159b72e69 | |||
81c23bbf9d | |||
0d5b8edf31 | |||
fcdb80830b | |||
50b48ab25c | |||
31b45666b0 | |||
233e76724a | |||
af637a82c3 | |||
ea32ea11f2 | |||
1b7a0de745 | |||
50e0cb65d9 | |||
ba4807f62c | |||
5efc02a238 | |||
8e50ac67bc | |||
a3c03e8ceb | |||
5a3e30b30a | |||
e3ab90042d | |||
f35c15f7d2 | |||
32387cd034 | |||
cf5c816483 | |||
bf9b9ca54c | |||
0ca2ca33c2 | |||
51f25e96e9 | |||
1875047638 | |||
fa4d61eaf0 | |||
49eb638e15 | |||
fc1f290b85 | |||
9194dc0161 | |||
0d480dbf7c | |||
183e83684a | |||
7b4ac7998a | |||
d75c6b0c36 | |||
40b222f8bc | |||
aa7dfb7bee | |||
6c1453eb54 | |||
c1845aec83 | |||
eb8479ac9a | |||
636c027298 | |||
02e187f066 | |||
854112095b | |||
a71c805959 | |||
c3ced0d089 | |||
80996ea63e | |||
aff51f8af1 | |||
ccbb81e9f5 | |||
f88dd28c51 | |||
a65a71df5d | |||
22f2ecc433 | |||
7f90ad7847 | |||
1292c0ecea | |||
55b7d5025b | |||
6a310bbaa9 | |||
bc8753da85 | |||
7f63e318f1 | |||
6c749319cf | |||
7a4463e104 | |||
e1be4ba925 | |||
34d21c1de3 | |||
fae36aebf4 | |||
75e828923a | |||
b499b87f8c | |||
233dbec4b3 | |||
d56ff9592e | |||
08f6317beb | |||
a75457ad88 | |||
b0482003bd | |||
634356e72f | |||
6d3cc16ab1 | |||
6027671c09 | |||
29d0cb4a15 | |||
fe7001975a | |||
ac88f1c146 | |||
b5b86218c5 | |||
bdcc6e52e6 | |||
0eae817aa6 | |||
8994b42760 | |||
6a63ce992a | |||
9ae6285eef | |||
8f9737f567 | |||
f287d313c3 | |||
e745836404 | |||
08baf798aa | |||
8bcb14c65d | |||
d94dc68830 | |||
297fed6aef | |||
d690d6e0e3 | |||
9ba8d88b07 | |||
34a40b0131 | |||
182bf5f2bd | |||
04638535d8 | |||
d87c8428fe | |||
166fb9a8e4 | |||
28a21d0b8f | |||
d1d1d60c30 | |||
80fd49d60b | |||
34eb1331a3 | |||
bff329a329 | |||
604929d002 | |||
4a9151e4aa | |||
020cc89576 | |||
25898d34ca | |||
6394388714 | |||
d4101c7bdf | |||
c93bf89cbe | |||
88d1f29fe2 | |||
c437a33f2a | |||
e3259f39f1 | |||
cb357b0a16 | |||
a7faf445c4 | |||
82a08f24c0 | |||
afa89ac125 | |||
2060b5cd34 | |||
d69730a333 | |||
9714a30148 | |||
23c0f2c313 | |||
5c4139be45 | |||
6b1a3a20e5 | |||
4ae00c80ca | |||
827792c4f0 | |||
abbe700dac | |||
1347bfe243 | |||
a76ee95b6d | |||
f3689f09cd | |||
d545cfd38c | |||
3631a9fac2 | |||
aee4ad2d3f | |||
f88c86c799 | |||
60ac27e401 | |||
d0567de4e6 | |||
ca30fd6088 | |||
1470e9d5ca | |||
f45efe2aa8 | |||
5b6c475817 | |||
4abd2d709f | |||
d97aff85b3 | |||
deec65446f | |||
5c662b1ae1 | |||
f648940388 | |||
5aae17754f | |||
0ef0f6ece1 | |||
bfd46f28e0 | |||
eaece18afc | |||
67d39b037c | |||
dd3f5a146d | |||
9fdc5b4b9d | |||
1f32d13698 | |||
886b1019ed | |||
3e8ed8a171 | |||
8307daee63 | |||
9b40d10352 | |||
f2a06eab37 | |||
74fd70416f | |||
b85c164195 | |||
75c41b645a | |||
54c8b3ef29 | |||
56bde40035 | |||
ff4a015baa | |||
0db4fcc27e | |||
f3080b6277 | |||
69cbbd5811 | |||
0b85760939 | |||
03f3a4805f | |||
d95adf2631 | |||
e971d40e06 | |||
c65a01a5f0 | |||
8586014e17 | |||
bdfae4ba04 | |||
75cb94b51a | |||
2f6d163a7a | |||
ecfe72bcad | |||
e6ff9e18cc | |||
3c550c1781 | |||
537693f5cf | |||
5ae0589547 | |||
71fc6fc257 | |||
c0d7b16ee6 | |||
f3f7aa9e1d | |||
43355970db | |||
bfa386acba | |||
e8b432485d | |||
a12a34e3bb | |||
b79855c01d | |||
17fe501a6d | |||
8201b367ec | |||
6c242084ca | |||
aefe7b176a | |||
6059b85e58 | |||
aa46c52eee | |||
d3cbfbdb59 | |||
cc9b77b876 |
24
.gitattributes
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
* text=auto
|
||||||
|
* text eol=lf
|
||||||
|
|
||||||
|
# Windows forced line-endings
|
||||||
|
/.idea/* text eol=crlf
|
||||||
|
|
||||||
|
# Gradle wrapper
|
||||||
|
*.jar binary
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.webp binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.gz binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.woff binary
|
||||||
|
*.pyc binary
|
||||||
|
*.swp binary
|
29
.github/CONTRIBUTING.md
vendored
@ -1,6 +1,17 @@
|
|||||||
|
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
|
||||||
|
|
||||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here
|
* 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)
|
||||||
@ -8,17 +19,9 @@
|
|||||||
* 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), include results and device names, OS, modifications (root, Xposed)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* **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)
|
||||||
* For multipart issues **use list** like this:
|
* Don't group unrelated requests into one issue
|
||||||
* [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
|
||||||
|
|
||||||
@ -28,7 +31,3 @@ 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
|
|
||||||
|
|
||||||
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
|
||||||
|
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github: inorichi
|
||||||
|
ko_fi: inorichi
|
18
.github/ISSUE_TEMPLATE.md
vendored
@ -1,7 +1,17 @@
|
|||||||
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
**DO NOT OPEN ISSUES/REQUESTS RELATING TO EXTENSIONS/CATALOGUES IN THIS REPOSITORY. Open them at the following repository https://github.com/inorichi/tachiyomi-extensions/**
|
||||||
|
|
||||||
Remove line above and describe your issue here. Fill out version below. Use Preview.
|
**For all other requests Please fill out the form below and remove the first 3 lines of this template**
|
||||||
|
|
||||||
|
**App version:**
|
||||||
|
|
||||||
Version: r000 or v0.0.0
|
**Android version:**
|
||||||
(other relevant info like OS)
|
|
||||||
|
**Issue/Request:**
|
||||||
|
|
||||||
|
**Steps to reproduce (if applicable)**
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
**Other details:**
|
||||||
|
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: "🐞 Bug report"
|
||||||
|
about: Report a bug
|
||||||
|
title: "[Bug] Write short description here"
|
||||||
|
labels: "bug"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Device information
|
||||||
|
* Tachiyomi version: ?
|
||||||
|
* Android version: ?
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
This should happen.
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
This happened instead.
|
||||||
|
|
||||||
|
### Other details
|
||||||
|
Additional details and attachments.
|
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: "🌟 Feature request"
|
||||||
|
about: Suggest a feature to improve Tachiyomi
|
||||||
|
title: "[Feature Request] Write short description here"
|
||||||
|
labels: "feature"
|
||||||
|
|
||||||
|
---
|
||||||
|
### Why/User Benefit/User Problem
|
||||||
|
(explain why this feature should be added)
|
||||||
|
|
||||||
|
### What/Requirements
|
||||||
|
(explain how this feature would behave)
|
BIN
.github/readme-images/app-icon.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
.github/readme-images/screens.png
vendored
Normal file
After Width: | Height: | Size: 1.0 MiB |
82
.travis.yml
@ -1,41 +1,59 @@
|
|||||||
|
dist: trusty
|
||||||
language: android
|
language: android
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- platform-tools
|
- build-tools-29.0.2
|
||||||
- tools
|
- android-28
|
||||||
|
- extra-android-m2repository
|
||||||
# The BuildTools version used by your project
|
- extra-google-m2repository
|
||||||
- build-tools-25.0.1
|
- extra-android-support
|
||||||
- android-25
|
- extra-google-google_play_services
|
||||||
- extra-android-m2repository
|
|
||||||
- extra-google-m2repository
|
|
||||||
- extra-android-support
|
|
||||||
- extra-google-google_play_services
|
|
||||||
|
|
||||||
licenses:
|
licenses:
|
||||||
- android-sdk-license-.+
|
- android-sdk-license-.+
|
||||||
- '.+'
|
|
||||||
|
|
||||||
jdk:
|
|
||||||
- oraclejdk8
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- chmod +x gradlew
|
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- mkdir "$ANDROID_HOME/licenses" || true
|
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license
|
||||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
||||||
|
tar xf secrets.tar;
|
||||||
#Build, and run tests
|
mv debug.keystore "$HOME/.android";
|
||||||
script: "./gradlew clean buildStandardDebug"
|
fi
|
||||||
sudo: false
|
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root
|
||||||
|
- 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:
|
||||||
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
global:
|
||||||
|
- 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=
|
||||||
|
22
.travis/build.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/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
|
15
.travis/deploy.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/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
|
73
.travis/google-services.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
BIN
.travis/secrets.tar.enc
Normal file
74
README.md
@ -1,24 +1,70 @@
|
|||||||
| Build | Download | F-Droid |
|
| Build | Stable | Dev | Contribute | Support Server |
|
||||||
|-------|----------|-------------|
|
|-------|----------|---------|------------|---------|
|
||||||
| [](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-dev-versions) |
|
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
## [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
|
||||||
|
|
||||||
* Online and offline reading
|
Features include:
|
||||||
* Configurable reader with multiple viewers and settings
|
* Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||||
* MyAnimeList support
|
* Local reading of downloaded manga
|
||||||
* Resume from the next unread chapter
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* Chapter filtering
|
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/explore/anime), and [Shikimori](https://shikimori.one) support
|
||||||
* 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).
|
||||||
|
|
||||||
|
## 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 website.](https://tachiyomi.org/)
|
||||||
|
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
3
app/.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
custom.gradle
|
custom.gradle
|
||||||
|
google-services.json
|
||||||
|
240
app/build.gradle
@ -3,10 +3,10 @@ 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'
|
||||||
|
|
||||||
if (file("custom.gradle").exists()) {
|
shortcutHelper.filePath = './shortcuts.xml'
|
||||||
apply from: "custom.gradle"
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
||||||
@ -29,17 +29,17 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileSdkVersion 28
|
||||||
buildToolsVersion "25.0.2"
|
buildToolsVersion '29.0.2'
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 28
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 24
|
versionCode 42
|
||||||
versionName "0.6.1"
|
versionName "0.8.5"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -48,6 +48,8 @@ android {
|
|||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
multiDexEnabled true
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||||
}
|
}
|
||||||
@ -57,25 +59,22 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
multiDexEnabled true
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
multiDexEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "default"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
standard {
|
standard {
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||||
|
dimension "default"
|
||||||
}
|
}
|
||||||
fdroid {
|
fdroid {
|
||||||
|
dimension "default"
|
||||||
}
|
}
|
||||||
dev {
|
dev {
|
||||||
minSdkVersion 21
|
|
||||||
resConfigs "en", "xxhdpi"
|
resConfigs "en", "xxhdpi"
|
||||||
|
dimension "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,132 +91,153 @@ android {
|
|||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
||||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '25.4.0'
|
final support_library_version = '28.0.0'
|
||||||
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:customtabs:$support_library_version"
|
implementation "com.android.support:support-annotations:$support_library_version"
|
||||||
|
implementation "com.android.support:customtabs:$support_library_version"
|
||||||
|
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
|
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.3.0'
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.8.1"
|
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
||||||
compile 'com.squareup.okio:okio:1.13.0'
|
implementation 'com.squareup.okio:okio:1.14.0'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.3.0'
|
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"
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.8.1'
|
implementation 'com.google.code.gson:gson:2.8.2'
|
||||||
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
// YAML
|
|
||||||
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
compile 'com.squareup.duktape:duktape-android:1.1.0'
|
implementation 'com.squareup.duktape:duktape-android:1.2.0'
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||||
compile 'com.github.seven332:unifile:1.0.0'
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
compile 'org.jsoup:jsoup:1.10.2'
|
implementation 'org.jsoup:jsoup:1.12.1'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
compile 'com.evernote:android-job:1.1.11'
|
implementation 'com.evernote:android-job:1.2.5'
|
||||||
compile 'com.google.android.gms:play-services-gcm:11.0.1'
|
implementation 'com.google.android.gms:play-services-gcm:11.8.0'
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:1.13.0"
|
implementation 'android.arch.persistence:db:1.1.1'
|
||||||
|
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||||
|
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||||
|
implementation 'io.requery:sqlite-android:3.25.2'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
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-v7:$nucleus_version"
|
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.8.0'
|
final glide_version = '4.6.1'
|
||||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
|
||||||
// Transformations
|
// Transformations
|
||||||
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
implementation 'jp.wasabeef:glide-transformations:3.1.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
compile 'ch.acra:acra:4.9.2'
|
implementation 'ch.acra:acra:4.9.2'
|
||||||
|
|
||||||
// Sort
|
|
||||||
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
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:5.0.0-rc1'
|
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.9.4.5'
|
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
|
||||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
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
|
// Conductor
|
||||||
compile "com.bluelinelabs:conductor:2.1.4"
|
implementation 'com.bluelinelabs:conductor:2.1.5'
|
||||||
compile 'com.github.inorichi:conductor-support-preference:9e36460'
|
implementation ("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
|
exclude group: "com.android.support"
|
||||||
|
}
|
||||||
|
implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
|
||||||
|
|
||||||
// RxBindings
|
// RxBindings
|
||||||
final rxbindings_version = '1.0.1'
|
final rxbindings_version = '1.0.1'
|
||||||
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||||
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||||
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||||
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testCompile 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:1.7.1'
|
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||||
|
|
||||||
final robolectric_version = '3.1.4'
|
final robolectric_version = '3.1.4'
|
||||||
testCompile "org.robolectric:robolectric:$robolectric_version"
|
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
||||||
testCompile "org.robolectric:shadows-multidex:$robolectric_version"
|
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||||
testCompile "org.robolectric:shadows-play-services:$robolectric_version"
|
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||||
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
final coroutines_version = '1.3.3'
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.1.3'
|
ext.kotlin_version = '1.3.61'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -230,49 +250,15 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround to force a support lib version
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
configurations.all {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
|
||||||
resolutionStrategy.eachDependency { details ->
|
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||||
def requested = details.requested
|
|
||||||
if (requested.group == 'com.android.support') {
|
|
||||||
if (!requested.name.startsWith("multidex")) {
|
|
||||||
details.useVersion '25.4.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add support for placeholders in resource files
|
androidExtensions {
|
||||||
//https://code.google.com/p/android/issues/detail?id=69224
|
experimental = true
|
||||||
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
|
|
||||||
def file = new File(basePath, fileName);
|
|
||||||
|
|
||||||
if (!file.exists()) {
|
|
||||||
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Replacing placeholders in " + file.toString())
|
|
||||||
logger.debug("Placeholders: " + placeholders.toString())
|
|
||||||
|
|
||||||
def content = file.getText('UTF-8')
|
|
||||||
|
|
||||||
placeholders.each { entry ->
|
|
||||||
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
file.write(content, 'UTF-8')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||||
android.applicationVariants.all { variant ->
|
apply plugin: 'com.google.gms.google-services'
|
||||||
variant.outputs.each { output ->
|
}
|
||||||
output.processResources.doFirst {
|
|
||||||
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
|
|
||||||
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
|
|
||||||
|
|
||||||
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
9
app/proguard-rules.pro
vendored
@ -6,6 +6,14 @@
|
|||||||
|
|
||||||
-keep class com.hippo.image.** { *; }
|
-keep class com.hippo.image.** { *; }
|
||||||
-keep interface 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
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
@ -16,6 +24,7 @@
|
|||||||
# 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 *;
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
android:shortcutShortLabel="@string/label_library">
|
android:shortcutShortLabel="@string/label_library">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
android:targetPackage="${applicationId}" />
|
|
||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@ -21,8 +20,7 @@
|
|||||||
android:shortcutShortLabel="@string/short_recent_updates">
|
android:shortcutShortLabel="@string/short_recent_updates">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
android:targetPackage="${applicationId}" />
|
|
||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@ -33,8 +31,7 @@
|
|||||||
android:shortcutShortLabel="@string/label_recent_manga">
|
android:shortcutShortLabel="@string/label_recent_manga">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
android:targetPackage="${applicationId}" />
|
|
||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@ -45,7 +42,6 @@
|
|||||||
android:shortcutShortLabel="@string/label_catalogues">
|
android:shortcutShortLabel="@string/label_catalogues">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
android:targetPackage="${applicationId}" />
|
|
||||||
</shortcut>
|
</shortcut>
|
||||||
</shortcuts>
|
</shortcuts>
|
34
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<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>
|
5
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 18 KiB |
@ -8,12 +8,17 @@
|
|||||||
<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="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<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:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@ -24,15 +29,23 @@
|
|||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<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>
|
||||||
<meta-data android:name="android.app.shortcuts"
|
<intent-filter>
|
||||||
android:resource="@xml/shortcuts"/>
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
||||||
|
<!--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.reader.ReaderActivity" />
|
||||||
android:theme="@style/Theme.Reader" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@ -51,6 +64,38 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.ShikimoriLoginActivity"
|
||||||
|
android:label="Shikimori">
|
||||||
|
<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="shikimori-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.BangumiLoginActivity"
|
||||||
|
android:label="Bangumi">
|
||||||
|
<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="bangumi-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
@ -62,16 +107,6 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="eu.kanade.tachiyomi.util.ZipContentProvider"
|
|
||||||
android:authorities="${applicationId}.zip-provider"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="eu.kanade.tachiyomi.util.RarContentProvider"
|
|
||||||
android:authorities="${applicationId}.rar-provider"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -85,7 +120,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.UpdateDownloaderService"
|
android:name=".data.updater.UpdaterService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
@ -96,10 +131,6 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
|
||||||
android:value="GlideModule" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@ -7,7 +7,8 @@ import android.support.multidex.MultiDex
|
|||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
@ -27,22 +28,21 @@ open class App : Application() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
Injekt = InjektScope(DefaultRegistrar())
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
|
||||||
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
setupJobManager()
|
setupJobManager()
|
||||||
|
setupNotificationChannels()
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
if (BuildConfig.DEBUG) {
|
MultiDex.install(this)
|
||||||
MultiDex.install(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
@ -55,14 +55,22 @@ open class App : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupJobManager() {
|
protected open fun setupJobManager() {
|
||||||
JobManager.create(this).addJobCreator { tag ->
|
try {
|
||||||
when (tag) {
|
JobManager.create(this).addJobCreator { tag ->
|
||||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
when (tag) {
|
||||||
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
||||||
BackupCreatorJob.TAG -> BackupCreatorJob()
|
UpdaterJob.TAG -> UpdaterJob()
|
||||||
else -> null
|
BackupCreatorJob.TAG -> BackupCreatorJob()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w("Can't initialize job manager")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun setupNotificationChannels() {
|
||||||
|
Notifications.createChannels(this)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,34 +8,55 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.*
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingleton(app)
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { ChapterCache(app) }
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { CoverCache(app) }
|
addSingletonFactory { ChapterCache(app) }
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app) }
|
addSingletonFactory { CoverCache(app) }
|
||||||
|
|
||||||
addSingletonFactory { SourceManager(app) }
|
addSingletonFactory { NetworkHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { DownloadManager(app) }
|
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
|
||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { ExtensionManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
object Constants {
|
|
||||||
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
|
|
||||||
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
|
|
||||||
const val NOTIFICATION_UPDATER_ID = 3
|
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
|
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
|
|
||||||
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
|
|
||||||
}
|
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
@ -25,7 +25,7 @@ object Migrations {
|
|||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to evernote's job scheduler.
|
// Restore jobs after upgrading to evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||||
UpdateCheckerJob.setupTask()
|
UpdaterJob.setupTask()
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask()
|
LibraryUpdateJob.setupTask()
|
||||||
}
|
}
|
||||||
@ -45,6 +45,16 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -4,17 +4,8 @@ import android.app.IntentService
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.set
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
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.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
|
||||||
import timber.log.Timber
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,8 +17,6 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
// Name of class
|
// Name of class
|
||||||
private const val NAME = "BackupCreateService"
|
private const val NAME = "BackupCreateService"
|
||||||
|
|
||||||
// Backup called from job
|
|
||||||
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
|
||||||
// Options for backup
|
// Options for backup
|
||||||
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
|
|
||||||
@ -46,14 +35,12 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
* Make a backup from library
|
* Make a backup from library
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param path path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
* @param isJob backup called from job
|
|
||||||
*/
|
*/
|
||||||
fun makeBackup(context: Context, uri: Uri, flags: Int, isJob: Boolean = false) {
|
fun makeBackup(context: Context, uri: Uri, flags: Int) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(EXTRA_IS_JOB, isJob)
|
|
||||||
putExtra(EXTRA_FLAGS, flags)
|
putExtra(EXTRA_FLAGS, flags)
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@ -68,97 +55,9 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
|
|
||||||
// Get values
|
// Get values
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
|
||||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||||
// Create backup
|
// Create backup
|
||||||
createBackupFromApp(uri, flags, isJob)
|
backupManager.createBackup(uri, flags, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
}
|
||||||
* Create backup Json file from database
|
|
||||||
*
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param isJob backup called from job
|
|
||||||
*/
|
|
||||||
fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
|
|
||||||
// Create root object
|
|
||||||
val root = JsonObject()
|
|
||||||
|
|
||||||
// Create information object
|
|
||||||
val information = JsonObject()
|
|
||||||
|
|
||||||
// Create manga array
|
|
||||||
val mangaEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create category array
|
|
||||||
val categoryEntries = JsonArray()
|
|
||||||
|
|
||||||
// Add value's to root
|
|
||||||
root[VERSION] = Backup.CURRENT_VERSION
|
|
||||||
root[MANGAS] = mangaEntries
|
|
||||||
root[CATEGORIES] = categoryEntries
|
|
||||||
|
|
||||||
backupManager.databaseHelper.inTransaction {
|
|
||||||
// Get manga from database
|
|
||||||
val mangas = backupManager.getFavoriteManga()
|
|
||||||
|
|
||||||
// Backup library manga and its dependencies
|
|
||||||
mangas.forEach { manga ->
|
|
||||||
mangaEntries.add(backupManager.backupMangaObject(manga, flags))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
|
||||||
backupManager.backupCategories(categoryEntries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// When BackupCreatorJob
|
|
||||||
if (isJob) {
|
|
||||||
// Get dir of file
|
|
||||||
val dir = UniFile.fromUri(this, uri)
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = backupManager.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 {
|
|
||||||
backupManager.parser.toJson(root, it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val file = UniFile.fromUri(this, uri)
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
file.openOutputStream().bufferedWriter().use {
|
|
||||||
backupManager.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())
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -8,15 +8,15 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class BackupCreatorJob : Job() {
|
class BackupCreatorJob : Job() {
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun onRunJob(params: Params): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
|
val backupManager = BackupManager(context)
|
||||||
|
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
BackupCreateService.makeBackup(context, uri, flags, true)
|
backupManager.createBackup(uri, flags, true)
|
||||||
return Result.SUCCESS
|
return Result.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +29,6 @@ class BackupCreatorJob : Job() {
|
|||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
JobRequest.Builder(TAG)
|
JobRequest.Builder(TAG)
|
||||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||||
.setPersisted(true)
|
|
||||||
.setUpdateCurrent(true)
|
.setUpdateCurrent(true)
|
||||||
.build()
|
.build()
|
||||||
.schedule()
|
.schedule()
|
||||||
@ -40,4 +39,4 @@ class BackupCreatorJob : Job() {
|
|||||||
JobManager.instance().cancelAllForTag(TAG)
|
JobManager.instance().cancelAllForTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
@ -11,6 +14,7 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HIST
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
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
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
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.CATEGORIES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
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.CURRENT_VERSION
|
||||||
@ -23,10 +27,13 @@ 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 eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
@ -41,6 +48,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
internal val sourceManager: SourceManager by injectLazy()
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracking manager
|
||||||
|
*/
|
||||||
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of parser
|
* Version of parser
|
||||||
*/
|
*/
|
||||||
@ -67,17 +79,101 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
parser = initParser()
|
parser = initParser()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initParser(): Gson {
|
private fun initParser(): Gson = when (version) {
|
||||||
return when (version) {
|
1 -> GsonBuilder().create()
|
||||||
1 -> GsonBuilder().create()
|
2 -> GsonBuilder()
|
||||||
2 -> GsonBuilder()
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
.create()
|
||||||
.create()
|
else -> throw Exception("Json version unknown")
|
||||||
else -> throw Exception("Json version unknown")
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun createBackup(uri: Uri, flags: Int, isJob: Boolean) {
|
||||||
|
// Create root object
|
||||||
|
val root = JsonObject()
|
||||||
|
|
||||||
|
// Create manga array
|
||||||
|
val mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Add value's to root
|
||||||
|
root[Backup.VERSION] = Backup.CURRENT_VERSION
|
||||||
|
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 {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,23 +396,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
val trackToUpdate = ArrayList<Track>()
|
val trackToUpdate = ArrayList<Track>()
|
||||||
|
|
||||||
for (track in tracks) {
|
for (track in tracks) {
|
||||||
var isInDatabase = false
|
val service = trackManager.getService(track.sync_id)
|
||||||
for (dbTrack in dbTracks) {
|
if (service != null && service.isLogged) {
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
var isInDatabase = false
|
||||||
// The sync is already in the db, only update its fields
|
for (dbTrack in dbTracks) {
|
||||||
if (track.remote_id != dbTrack.remote_id) {
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
dbTrack.remote_id = track.remote_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
|
||||||
}
|
}
|
||||||
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
|
||||||
isInDatabase = true
|
|
||||||
trackToUpdate.add(dbTrack)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
if (!isInDatabase) {
|
||||||
if (!isInDatabase) {
|
// Insert new sync. Let the db assign the id
|
||||||
// Insert new sync. Let the db assign the id
|
track.id = null
|
||||||
track.id = null
|
trackToUpdate.add(track)
|
||||||
trackToUpdate.add(track)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update database
|
// Update database
|
||||||
@ -361,32 +463,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*
|
*
|
||||||
* @return [Manga], null if not found
|
* @return [Manga], null if not found
|
||||||
*/
|
*/
|
||||||
internal fun getMangaFromDatabase(manga: Manga): Manga? {
|
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||||
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list containing manga from library
|
* Returns list containing manga from library
|
||||||
*
|
*
|
||||||
* @return [Manga] from library
|
* @return [Manga] from library
|
||||||
*/
|
*/
|
||||||
internal fun getFavoriteManga(): List<Manga> {
|
internal fun getFavoriteManga(): List<Manga> =
|
||||||
return databaseHelper.getFavoriteMangas().executeAsBlocking()
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts manga and returns id
|
* Inserts manga and returns id
|
||||||
*
|
*
|
||||||
* @return id of [Manga], null if not found
|
* @return id of [Manga], null if not found
|
||||||
*/
|
*/
|
||||||
internal fun insertManga(manga: Manga): Long? {
|
internal fun insertManga(manga: Manga): Long? =
|
||||||
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts list of chapters
|
* Inserts list of chapters
|
||||||
*/
|
*/
|
||||||
internal fun insertChapters(chapters: List<Chapter>) {
|
private fun insertChapters(chapters: List<Chapter>) {
|
||||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +494,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*
|
*
|
||||||
* @return number of backups selected by user
|
* @return number of backups selected by user
|
||||||
*/
|
*/
|
||||||
fun numberOfBackups(): Int {
|
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
|
||||||
return preferences.numberOfBackups().getOrDefault()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
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.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.chop
|
import eu.kanade.tachiyomi.util.chop
|
||||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||||
@ -32,7 +33,8 @@ import timber.log.Timber
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@ -49,9 +51,8 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isRunning(context: Context): Boolean {
|
private fun isRunning(context: Context): Boolean =
|
||||||
return context.isServiceRunning(BackupRestoreService::class.java)
|
context.isServiceRunning(BackupRestoreService::class.java)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a service to restore a backup from Json
|
* Starts a service to restore a backup from Json
|
||||||
@ -113,7 +114,13 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
lateinit var executor: ExecutorService
|
/**
|
||||||
|
* 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.
|
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||||
@ -142,9 +149,7 @@ class BackupRestoreService : Service() {
|
|||||||
/**
|
/**
|
||||||
* This method needs to be implemented, but it's not used/needed.
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
*/
|
*/
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service receives an intent.
|
* Method called when the service receives an intent.
|
||||||
@ -164,7 +169,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
subscription = Observable.using(
|
subscription = Observable.using(
|
||||||
{ db.lowLevel().beginTransaction() },
|
{ db.lowLevel().beginTransaction() },
|
||||||
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
|
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
|
||||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||||
.doAfterTerminate { stopSelf(startId) }
|
.doAfterTerminate { stopSelf(startId) }
|
||||||
.subscribeOn(Schedulers.from(executor))
|
.subscribeOn(Schedulers.from(executor))
|
||||||
@ -182,29 +187,33 @@ class BackupRestoreService : Service() {
|
|||||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
return Observable.just(Unit)
|
||||||
val json = JsonParser().parse(reader).asJsonObject
|
.map {
|
||||||
|
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||||
|
val json = JsonParser().parse(reader).asJsonObject
|
||||||
|
|
||||||
// Get parser version
|
// Get parser version
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
// Initialize manager
|
// Initialize manager
|
||||||
backupManager = BackupManager(this, version)
|
backupManager = BackupManager(this, version)
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
restoreProgress = 0
|
restoreProgress = 0
|
||||||
errors.clear()
|
errors.clear()
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
json.get(CATEGORIES)?.let {
|
json.get(CATEGORIES)?.let {
|
||||||
backupManager.restoreCategories(it.asJsonArray)
|
backupManager.restoreCategories(it.asJsonArray)
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.from(mangasJson)
|
mangasJson
|
||||||
|
}
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
.concatMap {
|
.concatMap {
|
||||||
val obj = it.asJsonObject
|
val obj = it.asJsonObject
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||||
@ -287,17 +296,17 @@ class BackupRestoreService : Service() {
|
|||||||
categories: List<String>, history: List<DHistory>,
|
categories: List<String>, history: List<DHistory>,
|
||||||
tracks: List<Track>): Observable<Manga>? {
|
tracks: List<Track>): Observable<Manga>? {
|
||||||
// Get source
|
// Get source
|
||||||
val source = backupManager.sourceManager.get(manga.source) ?: return null
|
val source = backupManager.sourceManager.getOrStub(manga.source)
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
if (dbManga == null) {
|
return if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
} else { // Manga in database
|
} else { // Manga in database
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,20 +326,18 @@ class BackupRestoreService : Service() {
|
|||||||
manga
|
manga
|
||||||
}
|
}
|
||||||
.filter { it.id != null }
|
.filter { it.id != null }
|
||||||
.flatMap { manga ->
|
.flatMap {
|
||||||
chapterFetchObservable(source, manga, chapters)
|
chapterFetchObservable(source, it, chapters)
|
||||||
// Convert to the manga that contains new chapters.
|
// Convert to the manga that contains new chapters.
|
||||||
.map { manga }
|
.map { manga }
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
// Restore categories
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
backupManager.restoreCategoriesForManga(it, categories)
|
}
|
||||||
|
.flatMap {
|
||||||
// Restore history
|
trackingFetchObservable(it, tracks)
|
||||||
backupManager.restoreHistoryForManga(history)
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(it, tracks)
|
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
@ -352,14 +359,12 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
// Restore categories
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
backupManager.restoreCategoriesForManga(it, categories)
|
}
|
||||||
|
.flatMap { manga ->
|
||||||
// Restore history
|
trackingFetchObservable(manga, tracks)
|
||||||
backupManager.restoreHistoryForManga(history)
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(it, tracks)
|
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
@ -367,6 +372,17 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
* [Observable] that fetches chapter information
|
||||||
*
|
*
|
||||||
@ -379,10 +395,33 @@ class BackupRestoreService : Service() {
|
|||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
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]
|
* Called to update dialog in [BackupConst]
|
||||||
@ -403,4 +442,4 @@ class BackupRestoreService : Service() {
|
|||||||
sendLocalBroadcast(intent)
|
sendLocalBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Json values
|
* Json values
|
||||||
*/
|
*/
|
||||||
object Backup {
|
object Backup {
|
||||||
const val CURRENT_VERSION = 2
|
const val CURRENT_VERSION = 2
|
||||||
const val MANGA = "manga"
|
const val MANGA = "manga"
|
||||||
const val MANGAS = "mangas"
|
const val MANGAS = "mangas"
|
||||||
const val TRACK = "track"
|
const val TRACK = "track"
|
||||||
const val CHAPTERS = "chapters"
|
const val CHAPTERS = "chapters"
|
||||||
const val CATEGORIES = "categories"
|
const val CATEGORIES = "categories"
|
||||||
const val HISTORY = "history"
|
const val HISTORY = "history"
|
||||||
const val VERSION = "version"
|
const val VERSION = "version"
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
fun getDefaultFilename(): String {
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.json"
|
return "tachiyomi_$date.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import android.telecom.DisconnectCause.REMOTE
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import com.google.gson.stream.JsonToken
|
import com.google.gson.stream.JsonToken
|
||||||
@ -11,9 +12,11 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|||||||
object TrackTypeAdapter {
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
private const val SYNC = "s"
|
private const val SYNC = "s"
|
||||||
private const val REMOTE = "r"
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
private const val TITLE = "t"
|
private const val TITLE = "t"
|
||||||
private const val LAST_READ = "l"
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
|
||||||
fun build(): TypeAdapter<TrackImpl> {
|
fun build(): TypeAdapter<TrackImpl> {
|
||||||
return typeAdapter {
|
return typeAdapter {
|
||||||
@ -23,10 +26,14 @@ object TrackTypeAdapter {
|
|||||||
value(it.title)
|
value(it.title)
|
||||||
name(SYNC)
|
name(SYNC)
|
||||||
value(it.sync_id)
|
value(it.sync_id)
|
||||||
name(REMOTE)
|
name(MEDIA)
|
||||||
value(it.remote_id)
|
value(it.media_id)
|
||||||
|
name(LIBRARY)
|
||||||
|
value(it.library_id)
|
||||||
name(LAST_READ)
|
name(LAST_READ)
|
||||||
value(it.last_chapter_read)
|
value(it.last_chapter_read)
|
||||||
|
name(TRACKING_URL)
|
||||||
|
value(it.tracking_url)
|
||||||
endObject()
|
endObject()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,8 +47,10 @@ object TrackTypeAdapter {
|
|||||||
when (name) {
|
when (name) {
|
||||||
TITLE -> track.title = nextString()
|
TITLE -> track.title = nextString()
|
||||||
SYNC -> track.sync_id = nextInt()
|
SYNC -> track.sync_id = nextInt()
|
||||||
REMOTE -> track.remote_id = nextInt()
|
MEDIA -> track.media_id = nextInt()
|
||||||
|
LIBRARY -> track.library_id = nextLong()
|
||||||
LAST_READ -> track.last_chapter_read = nextInt()
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
|
TRACKING_URL -> track.tracking_url = nextString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
private val diskCache = DiskLruCache.open(
|
private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
|
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_VALUE_COUNT,
|
||||||
PARAMETER_CACHE_SIZE)
|
PARAMETER_CACHE_SIZE)
|
||||||
@ -82,10 +81,10 @@ 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.substring(0, file.lastIndexOf("."))
|
val key = file.substringBeforeLast(".")
|
||||||
// Remove file from cache.
|
// Remove file from cache.
|
||||||
return diskCache.remove(key)
|
return diskCache.remove(key)
|
||||||
} catch (e: IOException) {
|
} catch (e: Exception) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ 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 = context.getExternalFilesDir("covers") ?:
|
||||||
|
File(context.filesDir, "covers").also { it.mkdirs() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
@ -59,7 +60,7 @@ class CoverCache(private val context: Context) {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Remove file.
|
// Remove file.
|
||||||
val file = getCoverFile(thumbnailUrl!!)
|
val file = getCoverFile(thumbnailUrl)
|
||||||
return file.exists() && file.delete()
|
return file.exists() && file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import android.arch.persistence.db.SupportSQLiteOpenHelper
|
||||||
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.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.*
|
||||||
|
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
@ -12,8 +14,13 @@ import eu.kanade.tachiyomi.data.database.queries.*
|
|||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context)
|
||||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
|
.callback(DbOpenCallback())
|
||||||
|
.build()
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
|
||||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import android.arch.persistence.db.SupportSQLiteDatabase
|
||||||
|
import android.arch.persistence.db.SupportSQLiteOpenHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import eu.kanade.tachiyomi.data.database.tables.*
|
import eu.kanade.tachiyomi.data.database.tables.*
|
||||||
|
|
||||||
class DbOpenHelper(context: Context)
|
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||||
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@ -17,10 +18,10 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 4
|
const val DATABASE_VERSION = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
execSQL(MangaTable.createTableQuery)
|
execSQL(MangaTable.createTableQuery)
|
||||||
execSQL(ChapterTable.createTableQuery)
|
execSQL(ChapterTable.createTableQuery)
|
||||||
execSQL(TrackTable.createTableQuery)
|
execSQL(TrackTable.createTableQuery)
|
||||||
@ -30,12 +31,13 @@ class DbOpenHelper(context: Context)
|
|||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
execSQL(MangaTable.createFavoriteIndexQuery)
|
execSQL(MangaTable.createLibraryIndexQuery)
|
||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
|
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
||||||
|
|
||||||
@ -51,9 +53,23 @@ class DbOpenHelper(context: Context)
|
|||||||
if (oldVersion < 4) {
|
if (oldVersion < 4) {
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
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)
|
||||||
|
}
|
||||||
|
if (oldVersion < 8) {
|
||||||
|
db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index")
|
||||||
|
db.execSQL(MangaTable.createLibraryIndexQuery)
|
||||||
|
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
db.setForeignKeyConstraintsEnabled(true)
|
db.setForeignKeyConstraintsEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ 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_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
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_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_SOURCE_ORDER
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||||
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
put(COL_URL, obj.url)
|
put(COL_URL, obj.url)
|
||||||
put(COL_NAME, obj.name)
|
put(COL_NAME, obj.name)
|
||||||
put(COL_READ, obj.read)
|
put(COL_READ, obj.read)
|
||||||
|
put(COL_SCANLATOR, obj.scanlator)
|
||||||
put(COL_BOOKMARK, obj.bookmark)
|
put(COL_BOOKMARK, obj.bookmark)
|
||||||
put(COL_DATE_FETCH, obj.date_fetch)
|
put(COL_DATE_FETCH, obj.date_fetch)
|
||||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||||
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
|||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
|
@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
interface BaseMangaGetResolver {
|
||||||
|
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||||
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||||
|
return mapBaseFromCursor(MangaImpl(), cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||||
|
@ -13,13 +13,15 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
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_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
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_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_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_SCORE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
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_SYNC_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
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_TOTAL_CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||||
|
|
||||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
@ -40,16 +42,19 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Track) = ContentValues(9).apply {
|
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
put(COL_SYNC_ID, obj.sync_id)
|
put(COL_SYNC_ID, obj.sync_id)
|
||||||
put(COL_REMOTE_ID, obj.remote_id)
|
put(COL_MEDIA_ID, obj.media_id)
|
||||||
|
put(COL_LIBRARY_ID, obj.library_id)
|
||||||
put(COL_TITLE, obj.title)
|
put(COL_TITLE, obj.title)
|
||||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
||||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
||||||
put(COL_STATUS, obj.status)
|
put(COL_STATUS, obj.status)
|
||||||
|
put(COL_TRACKING_URL, obj.tracking_url)
|
||||||
put(COL_SCORE, obj.score)
|
put(COL_SCORE, obj.score)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,12 +64,14 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
|||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||||
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_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))
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||||
|
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
|
|||||||
|
|
||||||
override lateinit var name: String
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
|
||||||
override var read: Boolean = false
|
override var read: Boolean = false
|
||||||
|
|
||||||
override var bookmark: Boolean = false
|
override var bookmark: Boolean = false
|
||||||
@ -29,9 +31,7 @@ class ChapterImpl : Chapter {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val chapter = other as Chapter
|
val chapter = other as Chapter
|
||||||
|
|
||||||
return url == chapter.url
|
return url == chapter.url
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class LibraryManga : MangaImpl() {
|
||||||
|
|
||||||
|
var unread: Int = 0
|
||||||
|
|
||||||
|
var category: Int = 0
|
||||||
|
|
||||||
|
}
|
@ -16,10 +16,6 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
var unread: Int
|
|
||||||
|
|
||||||
var category: Int
|
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
class MangaImpl : Manga {
|
open class MangaImpl : Manga {
|
||||||
|
|
||||||
override var id: Long? = null
|
override var id: Long? = null
|
||||||
|
|
||||||
@ -32,10 +32,6 @@ class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
@Transient override var unread: Int = 0
|
|
||||||
|
|
||||||
@Transient override var category: Int = 0
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
@ -10,7 +10,9 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var sync_id: Int
|
var sync_id: Int
|
||||||
|
|
||||||
var remote_id: Int
|
var media_id: Int
|
||||||
|
|
||||||
|
var library_id: Long?
|
||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var status: Int
|
var status: Int
|
||||||
|
|
||||||
|
var tracking_url: String
|
||||||
|
|
||||||
fun copyPersonalFrom(other: Track) {
|
fun copyPersonalFrom(other: Track) {
|
||||||
last_chapter_read = other.last_chapter_read
|
last_chapter_read = other.last_chapter_read
|
||||||
score = other.score
|
score = other.score
|
||||||
@ -29,7 +33,6 @@ interface Track : Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(serviceId: Int): Track = TrackImpl().apply {
|
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||||
sync_id = serviceId
|
sync_id = serviceId
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,9 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var sync_id: Int = 0
|
override var sync_id: Int = 0
|
||||||
|
|
||||||
override var remote_id: Int = 0
|
override var media_id: Int = 0
|
||||||
|
|
||||||
|
override var library_id: Long? = null
|
||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
@ -20,6 +22,8 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var status: Int = 0
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var tracking_url: String = ""
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
@ -28,13 +32,13 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
if (manga_id != other.manga_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (sync_id != other.sync_id) return false
|
if (sync_id != other.sync_id) return false
|
||||||
return remote_id == other.remote_id
|
return media_id == other.media_id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||||
result = 31 * result + sync_id
|
result = 31 * result + sync_id
|
||||||
result = 31 * result + remote_id
|
result = 31 * result + media_id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.*
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
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
|
||||||
@ -23,7 +22,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(LibraryManga::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, CategoryTable.TABLE)
|
||||||
@ -73,6 +72,21 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaFavorite(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaViewer(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaViewerPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
|
.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()
|
||||||
@ -101,4 +115,4 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
|
||||||
interface TrackQueries : DbProvider {
|
interface TrackQueries : DbProvider {
|
||||||
|
|
||||||
fun getTracks(manga: Manga) = db.get()
|
fun getTracks(manga: Manga) = db.get()
|
||||||
.listOfObjects(Track::class.java)
|
.listOfObjects(Track::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||||
|
|
||||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||||
|
|
||||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(DeleteQuery.builder()
|
||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||||
.whereArgs(manga.id, sync.id)
|
.whereArgs(manga.id, sync.id)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
}
|
}
|
@ -1,24 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class LibraryMangaGetResolver : MangaGetResolver() {
|
class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val INSTANCE = LibraryMangaGetResolver()
|
val INSTANCE = LibraryMangaGetResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
override fun mapFromCursor(cursor: Cursor): LibraryManga {
|
||||||
val manga = super.mapFromCursor(cursor)
|
val manga = LibraryManga()
|
||||||
|
|
||||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(unreadColumn)
|
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
|||||||
|
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 MangaTitlePutResolver : 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_TITLE, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
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,6 +14,8 @@ 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_BOOKMARK = "bookmark"
|
||||||
|
|
||||||
const val COL_DATE_FETCH = "date_fetch"
|
const val COL_DATE_FETCH = "date_fetch"
|
||||||
@ -32,6 +34,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_BOOKMARK BOOLEAN NOT NULL,
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||||
@ -46,10 +49,17 @@ object ChapterTable {
|
|||||||
val createMangaIdIndexQuery: String
|
val createMangaIdIndexQuery: String
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
|
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
|
||||||
|
|
||||||
|
val createUnreadChaptersIndexQuery: String
|
||||||
|
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
|
||||||
|
"WHERE $COL_READ = 0"
|
||||||
|
|
||||||
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
|
val bookmarkUpdateQuery: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ object MangaTable {
|
|||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
|
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
|
||||||
|
|
||||||
val createFavoriteIndexQuery: String
|
val createLibraryIndexQuery: String
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE)"
|
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
||||||
|
"WHERE $COL_FAVORITE = 1"
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,9 @@ object TrackTable {
|
|||||||
|
|
||||||
const val COL_SYNC_ID = "sync_id"
|
const val COL_SYNC_ID = "sync_id"
|
||||||
|
|
||||||
const val COL_REMOTE_ID = "remote_id"
|
const val COL_MEDIA_ID = "remote_id"
|
||||||
|
|
||||||
|
const val COL_LIBRARY_ID = "library_id"
|
||||||
|
|
||||||
const val COL_TITLE = "title"
|
const val COL_TITLE = "title"
|
||||||
|
|
||||||
@ -22,20 +24,29 @@ 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_REMOTE_ID INTEGER NOT NULL,
|
$COL_MEDIA_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"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,272 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,8 +7,10 @@ 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.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to manage chapter downloads in the application. It must be instantiated once
|
* This class is used to manage chapter downloads in the application. It must be instantiated once
|
||||||
@ -19,15 +21,30 @@ import rx.Observable
|
|||||||
*/
|
*/
|
||||||
class DownloadManager(context: Context) {
|
class DownloadManager(context: Context) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sources manager.
|
||||||
|
*/
|
||||||
|
private val sourceManager by injectLazy<SourceManager>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||||
*/
|
*/
|
||||||
private val provider = DownloadProvider(context)
|
private val provider = DownloadProvider(context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of downloaded chapters.
|
||||||
|
*/
|
||||||
|
private val cache = DownloadCache(context, provider, sourceManager)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloader whose only task is to download chapters.
|
* Downloader whose only task is to download chapters.
|
||||||
*/
|
*/
|
||||||
private val downloader = Downloader(context, provider)
|
private val downloader = Downloader(context, provider, cache, sourceManager)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue to delay the deletion of a list of chapters until triggered.
|
||||||
|
*/
|
||||||
|
private val pendingDeleter = DownloadPendingDeleter(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads queue, where the pending chapters are stored.
|
* Downloads queue, where the pending chapters are stored.
|
||||||
@ -80,9 +97,10 @@ class DownloadManager(context: Context) {
|
|||||||
*
|
*
|
||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param chapters the list of chapters to enqueue.
|
* @param chapters the list of chapters to enqueue.
|
||||||
|
* @param autoStart whether to start the downloader after enqueing the chapters.
|
||||||
*/
|
*/
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
|
||||||
downloader.queueChapters(manga, chapters)
|
downloader.queueChapters(manga, chapters, autoStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,7 +112,7 @@ class DownloadManager(context: Context) {
|
|||||||
* @return an observable containing the list of pages from the chapter.
|
* @return an observable containing the list of pages from the chapter.
|
||||||
*/
|
*/
|
||||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
|
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
|
||||||
return buildPageList(provider.findChapterDir(source, manga, chapter))
|
return buildPageList(provider.findChapterDir(chapter, manga, source))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,61 +138,73 @@ class DownloadManager(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the directory name for a manga.
|
* Returns true if the chapter is downloaded.
|
||||||
*
|
*
|
||||||
* @param manga the manga to query.
|
* @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 getMangaDirName(manga: Manga): String {
|
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean = false): Boolean {
|
||||||
return provider.getMangaDirName(manga)
|
return cache.isChapterDownloaded(chapter, manga, skipCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the directory name for the given chapter.
|
* Returns the amount of downloaded chapters for a manga.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter to query.
|
* @param manga the manga to check.
|
||||||
*/
|
*/
|
||||||
fun getChapterDirName(chapter: Chapter): String {
|
fun getDownloadCount(manga: Manga): Int {
|
||||||
return provider.getChapterDirName(chapter)
|
return cache.getDownloadCount(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the download directory for a source if it exists.
|
* Deletes the directories of a list of downloaded chapters.
|
||||||
*
|
*
|
||||||
* @param source the source to query.
|
* @param chapters the list of chapters to delete.
|
||||||
|
* @param manga the manga of the chapters.
|
||||||
|
* @param source the source of the chapters.
|
||||||
*/
|
*/
|
||||||
fun findSourceDir(source: Source): UniFile? {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
||||||
return provider.findSourceDir(source)
|
queue.remove(chapters)
|
||||||
|
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
|
||||||
|
chapterDirs.forEach { it.delete() }
|
||||||
|
cache.removeChapters(chapters, manga)
|
||||||
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
|
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the directory for the given manga, if it exists.
|
* Deletes the directory of a downloaded manga.
|
||||||
*
|
*
|
||||||
|
* @param manga the manga to delete.
|
||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
* @param manga the manga to query.
|
|
||||||
*/
|
*/
|
||||||
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
fun deleteManga(manga: Manga, source: Source) {
|
||||||
return provider.findMangaDir(source, manga)
|
queue.remove(manga)
|
||||||
|
provider.findMangaDir(manga, source)?.delete()
|
||||||
|
cache.removeManga(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the directory for the given chapter, if it exists.
|
* Adds a list of chapters to be deleted later.
|
||||||
*
|
*
|
||||||
* @param source the source of the chapter.
|
* @param chapters the list of chapters to delete.
|
||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapters.
|
||||||
* @param chapter the chapter to query.
|
|
||||||
*/
|
*/
|
||||||
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
return provider.findChapterDir(source, manga, chapter)
|
pendingDeleter.addChapters(chapters, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the directory of a downloaded chapter.
|
* Triggers the execution of the deletion of pending chapters.
|
||||||
*
|
|
||||||
* @param source the source of the chapter.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
* @param chapter the chapter to delete.
|
|
||||||
*/
|
*/
|
||||||
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
|
fun deletePendingChapters() {
|
||||||
provider.findChapterDir(source, manga, chapter)?.delete()
|
val pendingChapters = pendingDeleter.getPendingChapters()
|
||||||
|
for ((manga, chapters) in pendingChapters) {
|
||||||
|
val source = sourceManager.get(manga.source) ?: continue
|
||||||
|
deleteChapters(chapters, manga, source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import eu.kanade.tachiyomi.Constants
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
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.data.notification.NotificationHandler
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.chop
|
import eu.kanade.tachiyomi.util.chop
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* Notification builder.
|
* Notification builder.
|
||||||
*/
|
*/
|
||||||
private val notification by lazy {
|
private val notification by lazy {
|
||||||
NotificationCompat.Builder(context)
|
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
|
||||||
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,19 +36,13 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* The size of queue on start download.
|
* The size of queue on start download.
|
||||||
*/
|
*/
|
||||||
var initialQueueSize = 0
|
var initialQueueSize = 0
|
||||||
get() = field
|
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value != 0){
|
if (value != 0) {
|
||||||
isSingleChapter = (value == 1)
|
isSingleChapter = (value == 1)
|
||||||
}
|
}
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simultaneous download setting > 1.
|
|
||||||
*/
|
|
||||||
var multipleDownloadThreads = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
@ -69,7 +63,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*
|
*
|
||||||
* @param id the id of the notification.
|
* @param id the id of the notification.
|
||||||
*/
|
*/
|
||||||
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) {
|
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
|
||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,41 +80,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* those can only be dismissed by the user.
|
* those can only be dismissed by the user.
|
||||||
*/
|
*/
|
||||||
fun dismiss() {
|
fun dismiss() {
|
||||||
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID)
|
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when download progress changes.
|
* Called when download progress changes.
|
||||||
* Note: Only accepted when multi download active.
|
|
||||||
*
|
|
||||||
* @param queue the queue containing downloads.
|
|
||||||
*/
|
|
||||||
fun onProgressChange(queue: DownloadQueue) {
|
|
||||||
if (multipleDownloadThreads) {
|
|
||||||
doOnProgressChange(null, queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when download progress changes.
|
|
||||||
* Note: Only accepted when single download active.
|
|
||||||
*
|
*
|
||||||
* @param download download object containing download information.
|
* @param download download object containing download information.
|
||||||
* @param queue the queue containing downloads.
|
|
||||||
*/
|
*/
|
||||||
fun onProgressChange(download: Download, queue: DownloadQueue) {
|
fun onProgressChange(download: Download) {
|
||||||
if (!multipleDownloadThreads) {
|
|
||||||
doOnProgressChange(download, queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show notification progress of chapter.
|
|
||||||
*
|
|
||||||
* @param download download object containing download information.
|
|
||||||
* @param queue the queue containing downloads.
|
|
||||||
*/
|
|
||||||
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
|
|
||||||
// Create notification
|
// Create notification
|
||||||
with(notification) {
|
with(notification) {
|
||||||
// Check if first call.
|
// Check if first call.
|
||||||
@ -131,30 +99,19 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
isDownloading = true
|
isDownloading = true
|
||||||
|
// Pause action
|
||||||
|
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_pause),
|
||||||
|
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multipleDownloadThreads) {
|
val title = download.manga.title.chop(15)
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
val quotedTitle = Pattern.quote(title)
|
||||||
|
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
// Reset the queue size if the download progress is negative
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
if ((initialQueueSize - queue.size) < 0)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
initialQueueSize = queue.size
|
.format(download.downloadedImages, download.pages!!.size))
|
||||||
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
|
||||||
.format(initialQueueSize - queue.size, initialQueueSize))
|
|
||||||
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
|
||||||
} else {
|
|
||||||
download?.let {
|
|
||||||
val title = it.manga.title.chop(15)
|
|
||||||
val quotedTitle = Pattern.quote(title)
|
|
||||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
|
||||||
.format(it.downloadedImages, it.pages!!.size))
|
|
||||||
setProgress(it.pages!!.size, it.downloadedImages, false)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Displays the progress bar on notification
|
// Displays the progress bar on notification
|
||||||
notification.show()
|
notification.show()
|
||||||
@ -262,7 +219,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
errorThrown = true
|
errorThrown = true
|
||||||
|
@ -0,0 +1,180 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to keep a list of chapters for future deletion.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
class DownloadPendingDeleter(context: Context) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gson instance to encode and decode chapters.
|
||||||
|
*/
|
||||||
|
private val gson by injectLazy<Gson>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences used to store the list of chapters to delete.
|
||||||
|
*/
|
||||||
|
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last added chapter, used to avoid decoding from the preference too often.
|
||||||
|
*/
|
||||||
|
private var lastAddedEntry: Entry? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a list of chapters for future deletion.
|
||||||
|
*
|
||||||
|
* @param chapters the chapters to be deleted.
|
||||||
|
* @param manga the manga of the chapters.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun addChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
|
val lastEntry = lastAddedEntry
|
||||||
|
|
||||||
|
val newEntry = if (lastEntry != null && lastEntry.manga.id == manga.id) {
|
||||||
|
// Append new chapters
|
||||||
|
val newChapters = lastEntry.chapters.addUniqueById(chapters)
|
||||||
|
|
||||||
|
// If no chapters were added, do nothing
|
||||||
|
if (newChapters.size == lastEntry.chapters.size) return
|
||||||
|
|
||||||
|
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
||||||
|
lastEntry.copy(chapters = newChapters)
|
||||||
|
} else {
|
||||||
|
val existingEntry = prefs.getString(manga.id!!.toString(), null)
|
||||||
|
if (existingEntry != null) {
|
||||||
|
// Existing entry found on preferences, decode json and add the new chapter
|
||||||
|
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
||||||
|
|
||||||
|
// Append new chapters
|
||||||
|
val newChapters = savedEntry.chapters.addUniqueById(chapters)
|
||||||
|
|
||||||
|
// If no chapters were added, do nothing
|
||||||
|
if (newChapters.size == savedEntry.chapters.size) return
|
||||||
|
|
||||||
|
savedEntry.copy(chapters = newChapters)
|
||||||
|
} else {
|
||||||
|
// No entry has been found yet, create a new one
|
||||||
|
Entry(chapters.map { it.toEntry() }, manga.toEntry())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current state
|
||||||
|
val json = gson.toJson(newEntry)
|
||||||
|
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
|
||||||
|
lastAddedEntry = newEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of chapters to be deleted grouped by its manga.
|
||||||
|
*
|
||||||
|
* Note: the returned list of manga and chapters only contain basic information needed by the
|
||||||
|
* downloader, so don't use them for anything else.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
||||||
|
val entries = decodeAll()
|
||||||
|
prefs.edit().clear().apply()
|
||||||
|
lastAddedEntry = null
|
||||||
|
|
||||||
|
return entries.associate { entry ->
|
||||||
|
entry.manga.toModel() to entry.chapters.map { it.toModel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes all the chapters from preferences.
|
||||||
|
*/
|
||||||
|
private fun decodeAll(): List<Entry> {
|
||||||
|
return prefs.all.values.mapNotNull { rawEntry ->
|
||||||
|
try {
|
||||||
|
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of chapter entries ensuring no duplicates by chapter id.
|
||||||
|
*/
|
||||||
|
private fun List<ChapterEntry>.addUniqueById(chapters: List<Chapter>): List<ChapterEntry> {
|
||||||
|
val newList = toMutableList()
|
||||||
|
for (chapter in chapters) {
|
||||||
|
if (none { it.id == chapter.id }) {
|
||||||
|
newList.add(chapter.toEntry())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to save an entry of chapters with their manga into preferences.
|
||||||
|
*/
|
||||||
|
private data class Entry(
|
||||||
|
val chapters: List<ChapterEntry>,
|
||||||
|
val manga: MangaEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to save an entry for a chapter into preferences.
|
||||||
|
*/
|
||||||
|
private data class ChapterEntry(
|
||||||
|
val id: Long,
|
||||||
|
val url: String,
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to save an entry for a manga into preferences.
|
||||||
|
*/
|
||||||
|
private data class MangaEntry(
|
||||||
|
val id: Long,
|
||||||
|
val url: String,
|
||||||
|
val title: String,
|
||||||
|
val source: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga entry from a manga model.
|
||||||
|
*/
|
||||||
|
private fun Manga.toEntry(): MangaEntry {
|
||||||
|
return MangaEntry(id!!, url, title, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a chapter entry from a chapter model.
|
||||||
|
*/
|
||||||
|
private fun Chapter.toEntry(): ChapterEntry {
|
||||||
|
return ChapterEntry(id!!, url, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga model from a manga entry.
|
||||||
|
*/
|
||||||
|
private fun MangaEntry.toModel(): Manga {
|
||||||
|
return Manga.create(url, title, source).also {
|
||||||
|
it.id = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a chapter model from a chapter entry.
|
||||||
|
*/
|
||||||
|
private fun ChapterEntry.toModel(): Chapter {
|
||||||
|
return Chapter.create().also {
|
||||||
|
it.id = id
|
||||||
|
it.url = url
|
||||||
|
it.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
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.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -28,7 +29,9 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
||||||
UniFile.fromUri(context, Uri.parse(it))
|
val dir = UniFile.fromUri(context, Uri.parse(it))
|
||||||
|
DiskUtil.createNoMediaFile(dir, context)
|
||||||
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -40,13 +43,17 @@ class DownloadProvider(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Returns the download directory for a manga. For internal use only.
|
* Returns the download directory for a manga. For internal use only.
|
||||||
*
|
*
|
||||||
* @param source the source of the manga.
|
|
||||||
* @param manga the manga to query.
|
* @param manga the manga to query.
|
||||||
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
internal fun getMangaDir(source: Source, manga: Manga): UniFile {
|
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
||||||
return downloadsDir
|
try {
|
||||||
.createDirectory(getSourceDirName(source))
|
return downloadsDir
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getSourceDirName(source))
|
||||||
|
.createDirectory(getMangaDirName(manga))
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,10 +68,10 @@ class DownloadProvider(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Returns the download directory for a manga if it exists.
|
* Returns the download directory for a manga if it exists.
|
||||||
*
|
*
|
||||||
* @param source the source of the manga.
|
|
||||||
* @param manga the manga to query.
|
* @param manga the manga to query.
|
||||||
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
||||||
val sourceDir = findSourceDir(source)
|
val sourceDir = findSourceDir(source)
|
||||||
return sourceDir?.findFile(getMangaDirName(manga))
|
return sourceDir?.findFile(getMangaDirName(manga))
|
||||||
}
|
}
|
||||||
@ -72,15 +79,27 @@ class DownloadProvider(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Returns the download directory for a chapter if it exists.
|
* Returns the download directory for a chapter if it exists.
|
||||||
*
|
*
|
||||||
* @param source the source of the chapter.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
* @param chapter the chapter to query.
|
* @param chapter the chapter to query.
|
||||||
|
* @param manga the manga of the chapter.
|
||||||
|
* @param source the source of the chapter.
|
||||||
*/
|
*/
|
||||||
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(source, manga)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return mangaDir?.findFile(getChapterDirName(chapter))
|
return mangaDir?.findFile(getChapterDirName(chapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of downloaded directories for the chapters that exist.
|
||||||
|
*
|
||||||
|
* @param chapters the chapters to query.
|
||||||
|
* @param manga the manga of the chapter.
|
||||||
|
* @param source the source of the chapter.
|
||||||
|
*/
|
||||||
|
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||||
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
|
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the download directory name for a source.
|
* Returns the download directory name for a source.
|
||||||
*
|
*
|
||||||
@ -108,4 +127,4 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return DiskUtil.buildValidFilename(chapter.name)
|
return DiskUtil.buildValidFilename(chapter.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.NetworkInfo.State.CONNECTED
|
import android.net.NetworkInfo.State.CONNECTED
|
||||||
import android.net.NetworkInfo.State.DISCONNECTED
|
import android.net.NetworkInfo.State.DISCONNECTED
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.support.v4.app.NotificationCompat
|
||||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.connectivityManager
|
import eu.kanade.tachiyomi.util.connectivityManager
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
@ -41,7 +45,12 @@ class DownloadService : Service() {
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
context.startService(Intent(context, DownloadService::class.java))
|
val intent = Intent(context, DownloadService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,6 +90,7 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
listenDownloaderState()
|
listenDownloaderState()
|
||||||
@ -122,7 +132,7 @@ class DownloadService : Service() {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ state -> onNetworkStateChanged(state)
|
.subscribe({ state -> onNetworkStateChanged(state)
|
||||||
}, { error ->
|
}, {
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.download_queue_error)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
})
|
})
|
||||||
@ -176,4 +186,10 @@ class DownloadService : Service() {
|
|||||||
if (!isHeld) acquire()
|
if (!isHeld) acquire()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getPlaceholderNotification(): Notification {
|
||||||
|
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
|
||||||
|
.setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadStore(context: Context) {
|
class DownloadStore(
|
||||||
|
context: Context,
|
||||||
|
private val sourceManager: SourceManager
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference file where active downloads are stored.
|
* Preference file where active downloads are stored.
|
||||||
@ -26,11 +29,6 @@ class DownloadStore(context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database helper.
|
* Database helper.
|
||||||
*/
|
*/
|
||||||
@ -83,7 +81,7 @@ class DownloadStore(context: Context) {
|
|||||||
fun restore(): List<Download> {
|
fun restore(): List<Download> {
|
||||||
val objs = preferences.all
|
val objs = preferences.all
|
||||||
.mapNotNull { it.value as? String }
|
.mapNotNull { it.value as? String }
|
||||||
.map { deserialize(it) }
|
.mapNotNull { deserialize(it) }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
|
|
||||||
val downloads = mutableListOf<Download>()
|
val downloads = mutableListOf<Download>()
|
||||||
@ -119,8 +117,12 @@ class DownloadStore(context: Context) {
|
|||||||
*
|
*
|
||||||
* @param string the download as string.
|
* @param string the download as string.
|
||||||
*/
|
*/
|
||||||
private fun deserialize(string: String): DownloadObject {
|
private fun deserialize(string: String): DownloadObject? {
|
||||||
return gson.fromJson(string, DownloadObject::class.java)
|
return try {
|
||||||
|
gson.fromJson(string, DownloadObject::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,4 +134,4 @@ class DownloadStore(context: Context) {
|
|||||||
*/
|
*/
|
||||||
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,21 +9,18 @@ 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.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.*
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.BehaviorSubject
|
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading chapters.
|
* This class is the one in charge of downloading chapters.
|
||||||
@ -36,29 +33,26 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param provider the downloads directory provider.
|
* @param provider the downloads directory provider.
|
||||||
|
* @param cache the downloads cache, used to add the downloads to the cache after their completion.
|
||||||
|
* @param sourceManager the source manager.
|
||||||
*/
|
*/
|
||||||
class Downloader(private val context: Context, private val provider: DownloadProvider) {
|
class Downloader(
|
||||||
|
private val context: Context,
|
||||||
|
private val provider: DownloadProvider,
|
||||||
|
private val cache: DownloadCache,
|
||||||
|
private val sourceManager: SourceManager
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for persisting downloads across restarts.
|
* Store for persisting downloads across restarts.
|
||||||
*/
|
*/
|
||||||
private val store = DownloadStore(context)
|
private val store = DownloadStore(context, sourceManager)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue where active downloads are kept.
|
* Queue where active downloads are kept.
|
||||||
*/
|
*/
|
||||||
val queue = DownloadQueue(store)
|
val queue = DownloadQueue(store)
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences.
|
|
||||||
*/
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifier for the downloader state and progress.
|
* Notifier for the downloader state and progress.
|
||||||
*/
|
*/
|
||||||
@ -69,11 +63,6 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
*/
|
*/
|
||||||
private val subscriptions = CompositeSubscription()
|
private val subscriptions = CompositeSubscription()
|
||||||
|
|
||||||
/**
|
|
||||||
* Subject to do a live update of the number of simultaneous downloads.
|
|
||||||
*/
|
|
||||||
private val threadsSubject = BehaviorSubject.create<Int>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay to send a list of downloads to the downloader.
|
* Relay to send a list of downloads to the downloader.
|
||||||
*/
|
*/
|
||||||
@ -90,12 +79,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
@Volatile private var isRunning: Boolean = false
|
@Volatile private var isRunning: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Observable.fromCallable { store.restore() }
|
launchNow {
|
||||||
.map { downloads -> downloads.filter { isDownloadAllowed(it) } }
|
val chapters = async { store.restore() }
|
||||||
.subscribeOn(Schedulers.io())
|
queue.addAll(chapters.await())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.subscribe({ downloads -> queue.addAll(downloads)
|
|
||||||
}, { error -> Timber.e(error) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,7 +102,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
downloadsRelay.call(pending)
|
||||||
return !pending.isEmpty()
|
return pending.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,14 +167,8 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
|
|
||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
|
|
||||||
subscriptions += preferences.downloadThreads().asObservable()
|
subscriptions += downloadsRelay.concatMapIterable { it }
|
||||||
.subscribe {
|
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
|
||||||
threadsSubject.onNext(it)
|
|
||||||
notifier.multipleDownloadThreads = it > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions += downloadsRelay.flatMap { Observable.from(it) }
|
|
||||||
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ completeDownload(it)
|
.subscribe({ completeDownload(it)
|
||||||
@ -210,61 +191,51 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a download object for every chapter and adds them to the downloads queue. This method
|
* Creates a download object for every chapter and adds them to the downloads queue.
|
||||||
* must be called in the main thread.
|
|
||||||
*
|
*
|
||||||
* @param manga the manga of the chapters to download.
|
* @param manga the manga of the chapters to download.
|
||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
|
* @param autoStart whether to start the downloader after enqueing the chapters.
|
||||||
*/
|
*/
|
||||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
|
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
||||||
|
val wasEmpty = queue.isEmpty()
|
||||||
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
|
val chaptersWithoutDir = async {
|
||||||
|
val mangaDir = provider.findMangaDir(manga, source)
|
||||||
|
|
||||||
val chaptersToQueue = chapters
|
chapters
|
||||||
// Avoid downloading chapters with the same name.
|
// Avoid downloading chapters with the same name.
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
// Add chapters to queue from the start.
|
// Filter out those already downloaded.
|
||||||
.sortedByDescending { it.source_order }
|
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
||||||
// Create a downloader for each one.
|
// Add chapters to queue from the start.
|
||||||
.map { Download(source, manga, it) }
|
.sortedByDescending { it.source_order }
|
||||||
// Filter out those already queued or downloaded.
|
|
||||||
.filter { isDownloadAllowed(it) }
|
|
||||||
|
|
||||||
// Return if there's nothing to queue.
|
|
||||||
if (chaptersToQueue.isEmpty())
|
|
||||||
return
|
|
||||||
|
|
||||||
queue.addAll(chaptersToQueue)
|
|
||||||
|
|
||||||
// Initialize queue size.
|
|
||||||
notifier.initialQueueSize = queue.size
|
|
||||||
|
|
||||||
// Initial multi-thread
|
|
||||||
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
|
|
||||||
|
|
||||||
if (isRunning) {
|
|
||||||
// Send the list of downloads to the downloader.
|
|
||||||
downloadsRelay.call(chaptersToQueue)
|
|
||||||
} else {
|
|
||||||
// Show initial notification.
|
|
||||||
notifier.onProgressChange(queue)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Runs in main thread (synchronization needed).
|
||||||
* Returns true if the given download can be queued and downloaded.
|
val chaptersToQueue = chaptersWithoutDir.await()
|
||||||
*
|
// Filter out those already enqueued.
|
||||||
* @param download the download to be checked.
|
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||||
*/
|
// Create a download for each one.
|
||||||
private fun isDownloadAllowed(download: Download): Boolean {
|
.map { Download(source, manga, it) }
|
||||||
// If the chapter is already queued, don't add it again
|
|
||||||
if (queue.any { it.chapter.id == download.chapter.id })
|
|
||||||
return false
|
|
||||||
|
|
||||||
val dir = provider.findChapterDir(download.source, download.manga, download.chapter)
|
if (chaptersToQueue.isNotEmpty()) {
|
||||||
if (dir != null && dir.exists())
|
queue.addAll(chaptersToQueue)
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
// Initialize queue size.
|
||||||
|
notifier.initialQueueSize = queue.size
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
// Send the list of downloads to the downloader.
|
||||||
|
downloadsRelay.call(chaptersToQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start downloader if needed
|
||||||
|
if (autoStart && wasEmpty) {
|
||||||
|
DownloadService.start(this@Downloader.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,9 +243,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
*
|
*
|
||||||
* @param download the chapter to be downloaded.
|
* @param download the chapter to be downloaded.
|
||||||
*/
|
*/
|
||||||
private fun downloadChapter(download: Download): Observable<Download> {
|
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val mangaDir = provider.getMangaDir(download.source, download.manga)
|
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||||
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
|
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
@ -291,8 +262,8 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
Observable.just(download.pages!!)
|
Observable.just(download.pages!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageListObservable
|
pageListObservable
|
||||||
.doOnNext { pages ->
|
.doOnNext { _ ->
|
||||||
// Delete all temporary (unfinished) files
|
// Delete all temporary (unfinished) files
|
||||||
tmpDir.listFiles()
|
tmpDir.listFiles()
|
||||||
?.filter { it.name!!.endsWith(".tmp") }
|
?.filter { it.name!!.endsWith(".tmp") }
|
||||||
@ -306,18 +277,18 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
.doOnNext { notifier.onProgressChange(download, queue) }
|
.doOnNext { notifier.onProgressChange(download) }
|
||||||
.toList()
|
.toList()
|
||||||
.map { pages -> download }
|
.map { _ -> download }
|
||||||
// Do after download completes
|
// Do after download completes
|
||||||
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
|
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||||
// If the page list threw, it will resume here
|
// If the page list threw, it will resume here
|
||||||
.onErrorReturn { error ->
|
.onErrorReturn { error ->
|
||||||
download.status = Download.ERROR
|
download.status = Download.ERROR
|
||||||
notifier.onError(error.message, download.chapter.name)
|
notifier.onError(error.message, download.chapter.name)
|
||||||
download
|
download
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -407,7 +378,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
// Else guess from the uri.
|
// Else guess from the uri.
|
||||||
?: context.contentResolver.getType(file.uri)
|
?: context.contentResolver.getType(file.uri)
|
||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
?: DiskUtil.findImageMime { file.openInputStream() }
|
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
||||||
|
|
||||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
||||||
}
|
}
|
||||||
@ -416,10 +387,13 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
* Checks if the download was successful.
|
* Checks if the download was successful.
|
||||||
*
|
*
|
||||||
* @param download the download to check.
|
* @param download the download to check.
|
||||||
|
* @param mangaDir the manga directory of the download.
|
||||||
* @param tmpDir the directory where the download is currently stored.
|
* @param tmpDir the directory where the download is currently stored.
|
||||||
* @param dirname the real (non temporary) directory name of the download.
|
* @param dirname the real (non temporary) directory name of the download.
|
||||||
*/
|
*/
|
||||||
private fun ensureSuccessfulDownload(download: Download, tmpDir: UniFile, dirname: String) {
|
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
|
||||||
|
tmpDir: UniFile, dirname: String) {
|
||||||
|
|
||||||
// Ensure that the chapter folder has all the images.
|
// Ensure that the chapter folder has all the images.
|
||||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||||
|
|
||||||
@ -432,6 +406,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
// Only rename the directory if it's downloaded.
|
// Only rename the directory if it's downloaded.
|
||||||
if (download.status == Download.DOWNLOADED) {
|
if (download.status == Download.DOWNLOADED) {
|
||||||
tmpDir.renameTo(dirname)
|
tmpDir.renameTo(dirname)
|
||||||
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
|
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,7 +420,6 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
if (download.status == Download.DOWNLOADED) {
|
if (download.status == Download.DOWNLOADED) {
|
||||||
// remove downloaded chapter from queue
|
// remove downloaded chapter from queue
|
||||||
queue.remove(download)
|
queue.remove(download)
|
||||||
notifier.onProgressChange(queue)
|
|
||||||
}
|
}
|
||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||||
@ -460,4 +436,4 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
return queue.none { it.status <= Download.DOWNLOADING }
|
return queue.none { it.status <= Download.DOWNLOADING }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.download.model
|
|||||||
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
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.download.DownloadStore
|
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -40,6 +41,14 @@ class DownloadQueue(
|
|||||||
find { it.chapter.id == chapter.id }?.let { remove(it) }
|
find { it.chapter.id == chapter.id }?.let { remove(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun remove(chapters: List<Chapter>) {
|
||||||
|
for (chapter in chapters) { remove(chapter) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(manga: Manga) {
|
||||||
|
filter { it.manga.id == manga.id }.forEach { remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
queue.forEach { download ->
|
queue.forEach { download ->
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
@ -66,6 +75,7 @@ class DownloadQueue(
|
|||||||
val pageStatusSubject = PublishSubject.create<Int>()
|
val pageStatusSubject = PublishSubject.create<Int>()
|
||||||
setPagesSubject(download.pages, pageStatusSubject)
|
setPagesSubject(download.pages, pageStatusSubject)
|
||||||
return@flatMap pageStatusSubject
|
return@flatMap pageStatusSubject
|
||||||
|
.onBackpressureBuffer()
|
||||||
.filter { it == Page.READY }
|
.filter { it == Page.READY }
|
||||||
.map { download }
|
.map { download }
|
||||||
|
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.GlideBuilder
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import com.bumptech.glide.module.GlideModule
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to update Glide module settings
|
|
||||||
*/
|
|
||||||
class AppGlideModule : GlideModule {
|
|
||||||
|
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
|
||||||
// Set the cache size of Glide to 15 MiB
|
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide) {
|
|
||||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
|
||||||
|
|
||||||
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
|
||||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +1,51 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
import android.content.ContentValues.TAG
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import android.util.Log
|
||||||
import java.io.File
|
import com.bumptech.glide.Priority
|
||||||
import java.io.IOException
|
import com.bumptech.glide.load.DataSource
|
||||||
import java.io.InputStream
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import java.io.*
|
||||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
|
||||||
|
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||||
private var data: InputStream? = null
|
|
||||||
|
private var data: InputStream? = null
|
||||||
override fun loadData(priority: Priority): InputStream {
|
|
||||||
data = file.inputStream()
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
return data!!
|
loadFromFile(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
data?.let { data ->
|
try {
|
||||||
try {
|
data = FileInputStream(file)
|
||||||
data.close()
|
} catch (e: FileNotFoundException) {
|
||||||
} catch (e: IOException) {
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||||
// Ignore
|
Log.d(TAG, "Failed to open file", e)
|
||||||
}
|
}
|
||||||
}
|
callback.onLoadFailed(e)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
override fun cancel() {
|
|
||||||
// Do nothing.
|
callback.onDataReady(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getId(): String {
|
override fun cleanup() {
|
||||||
return file.toString()
|
try {
|
||||||
}
|
data?.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// Ignored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDataClass(): Class<InputStream> {
|
||||||
|
return InputStream::class.java
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDataSource(): DataSource {
|
||||||
|
return DataSource.LOCAL
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [DataFetcher] for loading a cover of a library manga.
|
||||||
|
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
||||||
|
* and copies the result to the cache.
|
||||||
|
*
|
||||||
|
* @param networkFetcher the network fetcher for this cover.
|
||||||
|
* @param manga the manga of the cover to load.
|
||||||
|
* @param file the file where this cover should be. It may exists or not.
|
||||||
|
*/
|
||||||
|
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||||
|
private val manga: Manga,
|
||||||
|
private val file: File)
|
||||||
|
: FileFetcher(file) {
|
||||||
|
|
||||||
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
|
if (!file.exists()) {
|
||||||
|
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
||||||
|
override fun onDataReady(data: InputStream?) {
|
||||||
|
if (data != null) {
|
||||||
|
val tmpFile = File(file.path + ".tmp")
|
||||||
|
try {
|
||||||
|
// Retrieve destination stream, create parent folders if needed.
|
||||||
|
val output = try {
|
||||||
|
tmpFile.outputStream()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
tmpFile.parentFile.mkdirs()
|
||||||
|
tmpFile.outputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file and rename to the original.
|
||||||
|
data.use { output.use { data.copyTo(output) } }
|
||||||
|
tmpFile.renameTo(file)
|
||||||
|
loadFromFile(callback)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
callback.onLoadFailed(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.onLoadFailed(Exception("Null data"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadFailed(e: Exception) {
|
||||||
|
callback.onLoadFailed(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
loadFromFile(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanup() {
|
||||||
|
super.cleanup()
|
||||||
|
networkFetcher.cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
super.cancel()
|
||||||
|
networkFetcher.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the id for this manga's cover.
|
|
||||||
*
|
|
||||||
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
|
|
||||||
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
|
|
||||||
* the file has changed. If the file doesn't exist it will append a 0.
|
|
||||||
*/
|
|
||||||
override fun getId(): String {
|
|
||||||
return manga.thumbnail_url + file.lastModified()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,24 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.*
|
||||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||||
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow:
|
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
||||||
*
|
*
|
||||||
* - Check in RAM LRU.
|
* - Check in RAM LRU.
|
||||||
* - Check in disk LRU.
|
* - Check in disk LRU.
|
||||||
@ -26,7 +27,7 @@ import java.io.InputStream
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cover cache where persistent covers are stored.
|
* Cover cache where persistent covers are stored.
|
||||||
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base network loader.
|
* Default network client.
|
||||||
*/
|
*/
|
||||||
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||||
InputStream::class.java, context)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
||||||
* and the file where it should be stored in case the manga is a favorite.
|
* and the file where it should be stored in case the manga is a favorite.
|
||||||
*/
|
*/
|
||||||
private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100)
|
private val lruCache = LruCache<GlideUrl, File>(100)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map where request headers are stored for a source.
|
* Map where request headers are stored for a source.
|
||||||
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
*/
|
*/
|
||||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||||
|
|
||||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
|
||||||
= MangaModelLoader(context)
|
return MangaModelLoader()
|
||||||
|
}
|
||||||
|
|
||||||
override fun teardown() {}
|
override fun teardown() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handles(model: Manga): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a fetcher for the given manga or null if the url is empty.
|
* Returns a fetcher for the given manga or null if the url is empty.
|
||||||
*
|
*
|
||||||
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
* @param width the width of the view where the resource will be loaded.
|
* @param width the width of the view where the resource will be loaded.
|
||||||
* @param height the height of the view where the resource will be loaded.
|
* @param height the height of the view where the resource will be loaded.
|
||||||
*/
|
*/
|
||||||
override fun getResourceFetcher(manga: Manga,
|
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
||||||
width: Int,
|
options: Options): ModelLoader.LoadData<InputStream>? {
|
||||||
height: Int): DataFetcher<InputStream>? {
|
|
||||||
|
|
||||||
// Check thumbnail is not null or empty
|
// Check thumbnail is not null or empty
|
||||||
val url = manga.thumbnail_url
|
val url = manga.thumbnail_url
|
||||||
if (url == null || url.isEmpty()) {
|
if (url == null || url.isEmpty()) {
|
||||||
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
|
|
||||||
if (url.startsWith("http")) {
|
if (url.startsWith("http")) {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
|
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||||
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
|
||||||
// and add them to the cache.
|
|
||||||
val (glideUrl, file) = lruCache.get(url) ?:
|
|
||||||
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
|
|
||||||
lruCache.put(url, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the resource fetcher for this request url.
|
// Get the resource fetcher for this request url.
|
||||||
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
|
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
||||||
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
|
|
||||||
|
if (!manga.favorite) {
|
||||||
|
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
||||||
|
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
|
||||||
|
|
||||||
|
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
return MangaUrlFetcher(networkFetcher, file, manga)
|
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||||
} else {
|
} else {
|
||||||
// Get the file from the url, removing the scheme if present.
|
// Get the file from the url, removing the scheme if present.
|
||||||
val file = File(url.substringAfter("file://"))
|
val file = File(url.substringAfter("file://"))
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
return MangaFileFetcher(file, manga)
|
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
|
||||||
|
val value = get(key)
|
||||||
|
return if (value == null) {
|
||||||
|
val answer = defaultValue()
|
||||||
|
put(key, answer)
|
||||||
|
answer
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.Key
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.io.File
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class MangaSignature(manga: Manga, file: File) : Key {
|
||||||
|
|
||||||
|
private val key = manga.thumbnail_url + file.lastModified()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return if (other is MangaSignature) {
|
||||||
|
key == other.key
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return key.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDiskCacheKey(md: MessageDigest) {
|
||||||
|
md.update(key.toByteArray(Key.CHARSET))
|
||||||
|
}
|
||||||
|
}
|
@ -1,71 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
|
|
||||||
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
|
|
||||||
* fallbacks to network and copies it to the cache.
|
|
||||||
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
|
|
||||||
* to network for fetching.
|
|
||||||
*
|
|
||||||
* @param networkFetcher the network fetcher for this cover.
|
|
||||||
* @param file the file where this cover should be. It may exists or not.
|
|
||||||
* @param manga the manga of the cover to load.
|
|
||||||
*/
|
|
||||||
class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
|
||||||
private val file: File,
|
|
||||||
private val manga: Manga)
|
|
||||||
: MangaFileFetcher(file, manga) {
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority): InputStream {
|
|
||||||
if (manga.favorite) {
|
|
||||||
synchronized(file) {
|
|
||||||
if (!file.exists()) {
|
|
||||||
val tmpFile = File(file.path + ".tmp")
|
|
||||||
try {
|
|
||||||
// Retrieve source stream.
|
|
||||||
val input = networkFetcher.loadData(priority)
|
|
||||||
?: throw Exception("Couldn't open source stream")
|
|
||||||
|
|
||||||
// Retrieve destination stream, create parent folders if needed.
|
|
||||||
val output = try {
|
|
||||||
tmpFile.outputStream()
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
tmpFile.parentFile.mkdirs()
|
|
||||||
tmpFile.outputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the file and rename to the original.
|
|
||||||
input.use { output.use { input.copyTo(output) } }
|
|
||||||
tmpFile.renameTo(file)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
tmpFile.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.loadData(priority)
|
|
||||||
} else {
|
|
||||||
if (file.exists()) {
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
return networkFetcher.loadData(priority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
networkFetcher.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
super.cleanup()
|
|
||||||
networkFetcher.cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,74 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.Options
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||||
|
|
||||||
|
override fun buildLoadData(
|
||||||
|
model: InputStream,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
options: Options
|
||||||
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
|
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handles(model: InputStream): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
|
||||||
|
|
||||||
|
override fun getDataClass(): Class<InputStream> {
|
||||||
|
return InputStream::class.java
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanup() {
|
||||||
|
try {
|
||||||
|
stream.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDataSource(): DataSource {
|
||||||
|
return DataSource.LOCAL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadData(
|
||||||
|
priority: Priority,
|
||||||
|
callback: DataFetcher.DataCallback<in InputStream>
|
||||||
|
) {
|
||||||
|
callback.onDataReady(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory class for creating [PassthroughModelLoader] instances.
|
||||||
|
*/
|
||||||
|
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
||||||
|
|
||||||
|
override fun build(
|
||||||
|
multiFactory: MultiModelLoaderFactory
|
||||||
|
): ModelLoader<InputStream, InputStream> {
|
||||||
|
return PassthroughModelLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun teardown() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.GlideBuilder
|
||||||
|
import com.bumptech.glide.Registry
|
||||||
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
|
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||||
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to update Glide module settings
|
||||||
|
*/
|
||||||
|
@GlideModule
|
||||||
|
class TachiGlideModule : AppGlideModule() {
|
||||||
|
|
||||||
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
|
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||||
|
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||||
|
builder.setDefaultTransitionOptions(Drawable::class.java,
|
||||||
|
DrawableTransitionOptions.withCrossFade())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
|
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||||
|
|
||||||
|
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||||
|
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||||
|
registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader
|
||||||
|
.Factory())
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() {
|
|||||||
.setRequiredNetworkType(wifiRestriction)
|
.setRequiredNetworkType(wifiRestriction)
|
||||||
.setRequiresCharging(acRestriction)
|
.setRequiresCharging(acRestriction)
|
||||||
.setRequirementsEnforced(true)
|
.setRequirementsEnforced(true)
|
||||||
.setPersisted(true)
|
|
||||||
.setUpdateCurrent(true)
|
.setUpdateCurrent(true)
|
||||||
.build()
|
.build()
|
||||||
.schedule()
|
.schedule()
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class will provide various functions to Rank mangas to efficiently schedule mangas to update.
|
||||||
|
*/
|
||||||
|
object LibraryUpdateRanker {
|
||||||
|
|
||||||
|
val rankingScheme = listOf(
|
||||||
|
(this::lexicographicRanking)(),
|
||||||
|
(this::latestFirstRanking)())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
||||||
|
* inactive [Manga] mInactive.
|
||||||
|
*
|
||||||
|
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
||||||
|
* @return a Comparator that ranks manga based on relevance.
|
||||||
|
*/
|
||||||
|
fun latestFirstRanking(): Comparator<Manga> {
|
||||||
|
return Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(mangaSecond.last_update, mangaFirst.last_update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Order the manga lexicographically.
|
||||||
|
* @return a Comparator that ranks manga lexicographically based on the title.
|
||||||
|
*/
|
||||||
|
fun lexicographicRanking(): Comparator<Manga> {
|
||||||
|
return Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(mangaFirst.title, mangaSecond.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,16 +10,18 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import eu.kanade.tachiyomi.Constants
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
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.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -80,10 +82,12 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
private val progressNotification by lazy { NotificationCompat.Builder(this)
|
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
||||||
|
.setContentTitle(getString(R.string.app_name))
|
||||||
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
.setLargeIcon(notificationBitmap)
|
.setLargeIcon(notificationBitmap)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +136,11 @@ class LibraryUpdateService(
|
|||||||
putExtra(KEY_TARGET, target)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +161,7 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
||||||
wakeLock.acquire()
|
wakeLock.acquire()
|
||||||
@ -196,7 +205,9 @@ class LibraryUpdateService(
|
|||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable
|
subscription = Observable
|
||||||
.defer {
|
.defer {
|
||||||
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
val mangaList = getMangaToUpdate(intent, target)
|
||||||
|
.sortedWith(rankingScheme[selectedScheme])
|
||||||
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
when (target) {
|
when (target) {
|
||||||
@ -224,7 +235,7 @@ class LibraryUpdateService(
|
|||||||
* @param target the target to update.
|
* @param target the target to update.
|
||||||
* @return a list of manga to update
|
* @return a list of manga to update
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> {
|
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1)
|
var listToUpdate = if (categoryId != -1)
|
||||||
@ -238,7 +249,6 @@ class LibraryUpdateService(
|
|||||||
else
|
else
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
@ -255,7 +265,7 @@ class LibraryUpdateService(
|
|||||||
* @param mangaToUpdate the list to update
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
|
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
// List containing new updates
|
// List containing new updates
|
||||||
@ -279,7 +289,7 @@ class LibraryUpdateService(
|
|||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
failedUpdates.add(manga)
|
failedUpdates.add(manga)
|
||||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
.filter { pair -> pair.first.isNotEmpty() }
|
.filter { pair -> pair.first.isNotEmpty() }
|
||||||
@ -296,9 +306,6 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
// Add manga with new chapters to the list.
|
// Add manga with new chapters to the list.
|
||||||
.doOnNext { manga ->
|
.doOnNext { manga ->
|
||||||
// Set last updated time
|
|
||||||
manga.last_update = Date().time
|
|
||||||
db.updateLastUpdated(manga).executeAsBlocking()
|
|
||||||
// Add to the list
|
// Add to the list
|
||||||
newUpdates.add(manga)
|
newUpdates.add(manga)
|
||||||
}
|
}
|
||||||
@ -325,7 +332,9 @@ class LibraryUpdateService(
|
|||||||
val dbChapters = chapters.map {
|
val dbChapters = chapters.map {
|
||||||
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
|
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
|
||||||
}
|
}
|
||||||
downloadManager.downloadChapters(manga, dbChapters)
|
// We don't want to start downloading while the library is updating, because websites
|
||||||
|
// may don't like it and they could ban the user.
|
||||||
|
downloadManager.downloadChapters(manga, dbChapters, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -347,7 +356,7 @@ class LibraryUpdateService(
|
|||||||
* @param mangaToUpdate the list to update
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
|
fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
|
|
||||||
@ -358,7 +367,7 @@ class LibraryUpdateService(
|
|||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
?: return@concatMap Observable.empty<Manga>()
|
?: return@concatMap Observable.empty<LibraryManga>()
|
||||||
|
|
||||||
source.fetchMangaDetails(manga)
|
source.fetchMangaDetails(manga)
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
@ -377,7 +386,7 @@ class LibraryUpdateService(
|
|||||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
* background thread, so it's safe to do heavy operations or network calls here.
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
*/
|
*/
|
||||||
private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> {
|
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
var count = 0
|
var count = 0
|
||||||
|
|
||||||
@ -417,7 +426,7 @@ class LibraryUpdateService(
|
|||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||||
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification
|
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
||||||
.setContentTitle(manga.title)
|
.setContentTitle(manga.title)
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build())
|
.build())
|
||||||
@ -434,7 +443,7 @@ class LibraryUpdateService(
|
|||||||
// Append new chapters from a previous, existing notification
|
// Append new chapters from a previous, existing notification
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val previousNotification = notificationManager.activeNotifications
|
val previousNotification = notificationManager.activeNotifications
|
||||||
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID }
|
.find { it.id == Notifications.ID_LIBRARY_RESULT }
|
||||||
|
|
||||||
if (previousNotification != null) {
|
if (previousNotification != null) {
|
||||||
val oldUpdates = previousNotification.notification.extras
|
val oldUpdates = previousNotification.notification.extras
|
||||||
@ -446,13 +455,14 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification {
|
notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
|
||||||
setSmallIcon(R.drawable.ic_book_white_24dp)
|
setSmallIcon(R.drawable.ic_book_white_24dp)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(getString(R.string.notification_new_chapters))
|
setContentTitle(getString(R.string.notification_new_chapters))
|
||||||
if (newUpdates.size > 1) {
|
if (newUpdates.size > 1) {
|
||||||
setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
|
setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
|
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
|
||||||
|
setNumber(newUpdates.size)
|
||||||
} else {
|
} else {
|
||||||
setContentText(newUpdates.first())
|
setContentText(newUpdates.first())
|
||||||
}
|
}
|
||||||
@ -466,7 +476,7 @@ class LibraryUpdateService(
|
|||||||
* Cancels the progress notification.
|
* Cancels the progress notification.
|
||||||
*/
|
*/
|
||||||
private fun cancelProgressNotification() {
|
private fun cancelProgressNotification() {
|
||||||
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -43,11 +44,10 @@ object NotificationHandler {
|
|||||||
* Returns [PendingIntent] that prompts user with apk install intent
|
* Returns [PendingIntent] that prompts user with apk install intent
|
||||||
*
|
*
|
||||||
* @param context context
|
* @param context context
|
||||||
* @param file file of apk that is installed
|
* @param uri uri of apk that is installed
|
||||||
*/
|
*/
|
||||||
fun installApkPendingActivity(context: Context, file: File): PendingIntent {
|
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
val uri = file.getUriCompat(context)
|
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import eu.kanade.tachiyomi.Constants
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -39,8 +38,15 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
// Resume the download service
|
// Resume the download service
|
||||||
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
|
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
|
||||||
|
// Pause the download service
|
||||||
|
ACTION_PAUSE_DOWNLOADS -> {
|
||||||
|
DownloadService.stop(context)
|
||||||
|
downloadManager.pauseDownloads()
|
||||||
|
}
|
||||||
// Clear the download queue
|
// Clear the download queue
|
||||||
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
||||||
|
// Show message notification created
|
||||||
|
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
|
||||||
// Launch share activity and dismiss notification
|
// Launch share activity and dismiss notification
|
||||||
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
@ -48,7 +54,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
// Cancel library update and dismiss notification
|
// Cancel library update and dismiss notification
|
||||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
|
||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
@ -158,9 +164,15 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to resume downloads.
|
// Called to resume downloads.
|
||||||
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||||
|
|
||||||
|
// Called to pause downloads.
|
||||||
|
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
||||||
|
|
||||||
// Called to clear downloads.
|
// Called to clear downloads.
|
||||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||||
|
|
||||||
|
// Called to notify user shortcut is created.
|
||||||
|
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
|
||||||
|
|
||||||
// Called to dismiss notification.
|
// Called to dismiss notification.
|
||||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||||
|
|
||||||
@ -186,6 +198,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that pauses the download queue
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun pauseDownloadsPendingBroadcast(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_PAUSE_DOWNLOADS
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [PendingIntent] that clears the download queue
|
* Returns a [PendingIntent] that clears the download queue
|
||||||
*
|
*
|
||||||
@ -199,6 +224,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun shortcutCreatedBroadcast(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_SHORTCUT_CREATED
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a service which dismissed the notification
|
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.notification
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to manage the basic information of all the notifications used in the app.
|
||||||
|
*/
|
||||||
|
object Notifications {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common notification channel and ids used anywhere.
|
||||||
|
*/
|
||||||
|
const val CHANNEL_COMMON = "common_channel"
|
||||||
|
const val ID_UPDATER = 1
|
||||||
|
const val ID_DOWNLOAD_IMAGE = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the library updater.
|
||||||
|
*/
|
||||||
|
const val CHANNEL_LIBRARY = "library_channel"
|
||||||
|
const val ID_LIBRARY_PROGRESS = 101
|
||||||
|
const val ID_LIBRARY_RESULT = 102
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the downloader.
|
||||||
|
*/
|
||||||
|
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||||
|
const val ID_DOWNLOAD_CHAPTER = 201
|
||||||
|
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
|
*
|
||||||
|
* @param context The application context.
|
||||||
|
*/
|
||||||
|
fun createChannels(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
|
val channels = listOf(
|
||||||
|
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||||
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
|
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
||||||
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
|
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
||||||
|
NotificationManager.IMPORTANCE_LOW)
|
||||||
|
)
|
||||||
|
context.notificationManager.createNotificationChannels(channels)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import android.support.v7.preference.PreferenceDataStore
|
||||||
|
|
||||||
|
class EmptyPreferenceDataStore : PreferenceDataStore() {
|
||||||
|
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInt(key: String?, defValue: Int): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String?, value: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String?, defValue: Long): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String?, value: Long) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFloat(key: String?, defValue: Float): Float {
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putFloat(key: String?, value: Float) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String?, defValue: String?): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String?, value: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String?, defValues: Set<String>?): Set<String>? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String?, values: Set<String>?) {
|
||||||
|
}
|
||||||
|
}
|
@ -1,116 +1,132 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class stores the keys for the preferences in the application.
|
* This class stores the keys for the preferences in the application.
|
||||||
*/
|
*/
|
||||||
object PreferenceKeys {
|
object PreferenceKeys {
|
||||||
|
|
||||||
const val theme = "pref_theme_key"
|
const val theme = "pref_theme_key"
|
||||||
|
|
||||||
const val rotation = "pref_rotation_type_key"
|
const val rotation = "pref_rotation_type_key"
|
||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
const val keepScreenOn = "pref_keep_screen_on_key"
|
const val trueColor = "pref_true_color_key"
|
||||||
|
|
||||||
const val customBrightness = "pref_custom_brightness_key"
|
const val fullscreen = "fullscreen"
|
||||||
|
|
||||||
const val customBrightnessValue = "custom_brightness_value"
|
const val keepScreenOn = "pref_keep_screen_on_key"
|
||||||
|
|
||||||
const val colorFilter = "pref_color_filter_key"
|
const val customBrightness = "pref_custom_brightness_key"
|
||||||
|
|
||||||
const val colorFilterValue = "color_filter_value"
|
const val customBrightnessValue = "custom_brightness_value"
|
||||||
|
|
||||||
const val defaultViewer = "pref_default_viewer_key"
|
const val colorFilter = "pref_color_filter_key"
|
||||||
|
|
||||||
const val imageScaleType = "pref_image_scale_type_key"
|
const val colorFilterValue = "color_filter_value"
|
||||||
|
|
||||||
const val imageDecoder = "image_decoder"
|
const val colorFilterMode = "color_filter_mode"
|
||||||
|
|
||||||
const val zoomStart = "pref_zoom_start_key"
|
const val defaultViewer = "pref_default_viewer_key"
|
||||||
|
|
||||||
const val readerTheme = "pref_reader_theme_key"
|
const val imageScaleType = "pref_image_scale_type_key"
|
||||||
|
|
||||||
const val cropBorders = "crop_borders"
|
const val zoomStart = "pref_zoom_start_key"
|
||||||
|
|
||||||
const val readWithTapping = "reader_tap"
|
const val readerTheme = "pref_reader_theme_key"
|
||||||
|
|
||||||
const val readWithVolumeKeys = "reader_volume_keys"
|
const val cropBorders = "crop_borders"
|
||||||
|
|
||||||
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
const val cropBordersWebtoon = "crop_borders_webtoon"
|
||||||
|
|
||||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
const val readWithTapping = "reader_tap"
|
||||||
|
|
||||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
const val readWithLongTap = "reader_long_tap"
|
||||||
|
|
||||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
const val readWithVolumeKeys = "reader_volume_keys"
|
||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
||||||
|
|
||||||
const val askUpdateTrack = "pref_ask_update_manga_sync_key"
|
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||||
|
|
||||||
const val lastUsedCatalogueSource = "last_catalogue_source"
|
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||||
|
|
||||||
const val catalogueAsList = "pref_display_catalogue_as_list"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
const val enabledLanguages = "source_languages"
|
const val lastUsedCatalogueSource = "last_catalogue_source"
|
||||||
|
|
||||||
const val backupDirectory = "backup_directory"
|
const val lastUsedCategory = "last_used_category"
|
||||||
|
|
||||||
const val downloadsDirectory = "download_directory"
|
const val catalogueAsList = "pref_display_catalogue_as_list"
|
||||||
|
|
||||||
const val downloadThreads = "pref_download_slots_key"
|
const val enabledLanguages = "source_languages"
|
||||||
|
|
||||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
const val backupDirectory = "backup_directory"
|
||||||
|
|
||||||
const val numberOfBackups = "backup_slots"
|
const val downloadsDirectory = "download_directory"
|
||||||
|
|
||||||
const val backupInterval = "backup_interval"
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
const val removeAfterReadSlots = "remove_after_read_slots"
|
const val numberOfBackups = "backup_slots"
|
||||||
|
|
||||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
const val backupInterval = "backup_interval"
|
||||||
|
|
||||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
const val removeAfterReadSlots = "remove_after_read_slots"
|
||||||
|
|
||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
const val libraryUpdateCategories = "library_update_categories"
|
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||||
|
|
||||||
const val filterDownloaded = "pref_filter_downloaded_key"
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
|
|
||||||
const val filterUnread = "pref_filter_unread_key"
|
const val libraryUpdateCategories = "library_update_categories"
|
||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||||
|
|
||||||
const val automaticUpdates = "automatic_updates"
|
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val filterUnread = "pref_filter_unread_key"
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val filterCompleted = "pref_filter_completed_key"
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
|
||||||
const val libraryAsList = "pref_display_library_as_list"
|
const val automaticUpdates = "automatic_updates"
|
||||||
|
|
||||||
const val lang = "app_language"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
const val libraryAsList = "pref_display_library_as_list"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
const val lang = "app_language"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
}
|
const val downloadBadge = "display_download_badge"
|
||||||
|
|
||||||
|
@Deprecated("Use the preferences of the source")
|
||||||
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
|
@Deprecated("Use the preferences of the source")
|
||||||
|
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
|
fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
|
||||||
|
|
||||||
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -39,8 +39,12 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
|
fun doubleTapAnimSpeed() = rxPrefs.getInteger(Keys.doubleTapAnimationSpeed, 500)
|
||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
|
fun trueColor() = rxPrefs.getBoolean(Keys.trueColor, false)
|
||||||
|
|
||||||
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
||||||
|
|
||||||
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
||||||
@ -53,20 +57,24 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
||||||
|
|
||||||
|
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
||||||
|
|
||||||
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
||||||
|
|
||||||
fun imageDecoder() = rxPrefs.getInteger(Keys.imageDecoder, 0)
|
|
||||||
|
|
||||||
fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1)
|
fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1)
|
||||||
|
|
||||||
fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0)
|
fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0)
|
||||||
|
|
||||||
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
|
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
|
||||||
|
|
||||||
|
fun cropBordersWebtoon() = rxPrefs.getBoolean(Keys.cropBordersWebtoon, false)
|
||||||
|
|
||||||
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
|
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
|
||||||
|
|
||||||
|
fun readWithLongTap() = rxPrefs.getBoolean(Keys.readWithLongTap, true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||||
|
|
||||||
fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
||||||
@ -79,8 +87,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun askUpdateTrack() = prefs.getBoolean(Keys.askUpdateTrack, false)
|
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0)
|
||||||
@ -115,14 +121,12 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
||||||
|
|
||||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10")
|
||||||
|
|
||||||
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadThreads() = rxPrefs.getInteger(Keys.downloadThreads, 1)
|
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1)
|
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1)
|
||||||
@ -139,12 +143,18 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
|
|
||||||
|
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||||
|
|
||||||
|
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
|
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||||
|
|
||||||
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
|
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
|
||||||
|
|
||||||
|
fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false)
|
||||||
|
|
||||||
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
|
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
|
||||||
|
|
||||||
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
||||||
@ -161,4 +171,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
|
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
||||||
|
|
||||||
|
fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet())
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.support.v7.preference.PreferenceDataStore
|
||||||
|
|
||||||
|
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
||||||
|
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return prefs.getBoolean(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
prefs.edit().putBoolean(key, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInt(key: String?, defValue: Int): Int {
|
||||||
|
return prefs.getInt(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String?, value: Int) {
|
||||||
|
prefs.edit().putInt(key, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String?, defValue: Long): Long {
|
||||||
|
return prefs.getLong(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String?, value: Long) {
|
||||||
|
prefs.edit().putLong(key, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFloat(key: String?, defValue: Float): Float {
|
||||||
|
return prefs.getFloat(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putFloat(key: String?, value: Float) {
|
||||||
|
prefs.edit().putFloat(key, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String?, defValue: String?): String? {
|
||||||
|
return prefs.getString(key, defValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String?, value: String?) {
|
||||||
|
prefs.edit().putString(key, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||||
|
return prefs.getStringSet(key, defValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||||
|
prefs.edit().putStringSet(key, values).apply()
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1,36 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||||
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
class TrackManager(private val context: Context) {
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
|
||||||
companion object {
|
class TrackManager(private val context: Context) {
|
||||||
const val MYANIMELIST = 1
|
|
||||||
const val ANILIST = 2
|
companion object {
|
||||||
const val KITSU = 3
|
const val MYANIMELIST = 1
|
||||||
}
|
const val ANILIST = 2
|
||||||
|
const val KITSU = 3
|
||||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
const val SHIKIMORI = 4
|
||||||
|
const val BANGUMI = 5
|
||||||
val aniList = Anilist(context, ANILIST)
|
}
|
||||||
|
|
||||||
val kitsu = Kitsu(context, KITSU)
|
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||||
|
|
||||||
val services = listOf(myAnimeList, aniList, kitsu)
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
val kitsu = Kitsu(context, KITSU)
|
||||||
|
|
||||||
fun hasLoggedServices() = services.any { it.isLogged }
|
val shikimori = Shikimori(context, SHIKIMORI)
|
||||||
|
|
||||||
}
|
val bangumi = Bangumi(context, BANGUMI)
|
||||||
|
|
||||||
|
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||||
|
|
||||||
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
fun hasLoggedServices() = services.any { it.isLogged }
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,70 +1,70 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.support.annotation.CallSuper
|
import android.support.annotation.CallSuper
|
||||||
import android.support.annotation.DrawableRes
|
import android.support.annotation.DrawableRes
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import okhttp3.OkHttpClient
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import rx.Completable
|
import okhttp3.OkHttpClient
|
||||||
import rx.Observable
|
import rx.Completable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
abstract class TrackService(val id: Int) {
|
|
||||||
|
abstract class TrackService(val id: Int) {
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
val networkService: NetworkHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
val networkService: NetworkHelper by injectLazy()
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = networkService.client
|
open val client: OkHttpClient
|
||||||
|
get() = networkService.client
|
||||||
// Name of the manga sync service to display
|
|
||||||
abstract val name: String
|
// Name of the manga sync service to display
|
||||||
|
abstract val name: String
|
||||||
@DrawableRes
|
|
||||||
abstract fun getLogo(): Int
|
@DrawableRes
|
||||||
|
abstract fun getLogo(): Int
|
||||||
abstract fun getLogoColor(): Int
|
|
||||||
|
abstract fun getLogoColor(): Int
|
||||||
abstract fun getStatusList(): List<Int>
|
|
||||||
|
abstract fun getStatusList(): List<Int>
|
||||||
abstract fun getStatus(status: Int): String
|
|
||||||
|
abstract fun getStatus(status: Int): String
|
||||||
abstract fun getScoreList(): List<String>
|
|
||||||
|
abstract fun getScoreList(): List<String>
|
||||||
open fun indexToScore(index: Int): Float {
|
|
||||||
return index.toFloat()
|
open fun indexToScore(index: Int): Float {
|
||||||
}
|
return index.toFloat()
|
||||||
|
}
|
||||||
abstract fun displayScore(track: Track): String
|
|
||||||
|
abstract fun displayScore(track: Track): String
|
||||||
abstract fun add(track: Track): Observable<Track>
|
|
||||||
|
abstract fun add(track: Track): Observable<Track>
|
||||||
abstract fun update(track: Track): Observable<Track>
|
|
||||||
|
abstract fun update(track: Track): Observable<Track>
|
||||||
abstract fun bind(track: Track): Observable<Track>
|
|
||||||
|
abstract fun bind(track: Track): Observable<Track>
|
||||||
abstract fun search(query: String): Observable<List<Track>>
|
|
||||||
|
abstract fun search(query: String): Observable<List<TrackSearch>>
|
||||||
abstract fun refresh(track: Track): Observable<Track>
|
|
||||||
|
abstract fun refresh(track: Track): Observable<Track>
|
||||||
abstract fun login(username: String, password: String): Completable
|
|
||||||
|
abstract fun login(username: String, password: String): Completable
|
||||||
@CallSuper
|
|
||||||
open fun logout() {
|
@CallSuper
|
||||||
preferences.setTrackCredentials(this, "", "")
|
open fun logout() {
|
||||||
}
|
preferences.setTrackCredentials(this, "", "")
|
||||||
|
}
|
||||||
open val isLogged: Boolean
|
|
||||||
get() = !getUsername().isEmpty() &&
|
open val isLogged: Boolean
|
||||||
!getPassword().isEmpty()
|
get() = !getUsername().isEmpty() &&
|
||||||
|
!getPassword().isEmpty()
|
||||||
fun getUsername() = preferences.trackUsername(this)
|
|
||||||
|
fun getUsername() = preferences.trackUsername(this)!!
|
||||||
fun getPassword() = preferences.trackPassword(this)
|
|
||||||
|
fun getPassword() = preferences.trackPassword(this)!!
|
||||||
fun saveCredentials(username: String, password: String) {
|
|
||||||
preferences.setTrackCredentials(this, username, password)
|
fun saveCredentials(username: String, password: String) {
|
||||||
}
|
preferences.setTrackCredentials(this, username, password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,160 +1,214 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import eu.kanade.tachiyomi.R
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import rx.Completable
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import rx.Observable
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import rx.Completable
|
||||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
const val COMPLETED = 2
|
|
||||||
const val ON_HOLD = 3
|
companion object {
|
||||||
const val DROPPED = 4
|
const val READING = 1
|
||||||
const val PLAN_TO_READ = 5
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
const val DEFAULT_STATUS = READING
|
const val DROPPED = 4
|
||||||
const val DEFAULT_SCORE = 0
|
const val PLANNING = 5
|
||||||
}
|
const val REPEATING = 6
|
||||||
|
|
||||||
override val name = "AniList"
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
|
||||||
|
const val POINT_100 = "POINT_100"
|
||||||
private val api by lazy { AnilistApi(client, interceptor) }
|
const val POINT_10 = "POINT_10"
|
||||||
|
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
||||||
override fun getLogo() = R.drawable.al
|
const val POINT_5 = "POINT_5"
|
||||||
|
const val POINT_3 = "POINT_3"
|
||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
}
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override val name = "AniList"
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
|
||||||
}
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
|
||||||
when (status) {
|
|
||||||
READING -> getString(R.string.reading)
|
private val api by lazy { AnilistApi(client, interceptor) }
|
||||||
COMPLETED -> getString(R.string.completed)
|
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
private val scorePreference = preferences.anilistScoreType()
|
||||||
DROPPED -> getString(R.string.dropped)
|
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
init {
|
||||||
else -> ""
|
// If the preference is an int from APIv1, logout user to force using APIv2
|
||||||
}
|
try {
|
||||||
}
|
scorePreference.get()
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
override fun getScoreList(): List<String> {
|
logout()
|
||||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
scorePreference.delete()
|
||||||
// 10 point
|
}
|
||||||
0 -> IntRange(0, 10).map(Int::toString)
|
}
|
||||||
// 100 point
|
|
||||||
1 -> IntRange(0, 100).map(Int::toString)
|
override fun getLogo() = R.drawable.al
|
||||||
// 5 stars
|
|
||||||
2 -> IntRange(0, 5).map { "$it ★" }
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
// Smiley
|
|
||||||
3 -> listOf("-", "😦", "😐", "😊")
|
override fun getStatusList(): List<Int> {
|
||||||
// 10 point decimal
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
|
||||||
4 -> IntRange(0, 100).map { (it / 10f).toString() }
|
}
|
||||||
else -> throw Exception("Unknown score type")
|
|
||||||
}
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
}
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
override fun indexToScore(index: Int): Float {
|
COMPLETED -> getString(R.string.completed)
|
||||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
// 10 point
|
DROPPED -> getString(R.string.dropped)
|
||||||
0 -> index * 10f
|
PLANNING -> getString(R.string.plan_to_read)
|
||||||
// 100 point
|
REPEATING -> getString(R.string.repeating)
|
||||||
1 -> index.toFloat()
|
else -> ""
|
||||||
// 5 stars
|
}
|
||||||
2 -> index * 20f
|
}
|
||||||
// Smiley
|
|
||||||
3 -> index * 30f
|
override fun getScoreList(): List<String> {
|
||||||
// 10 point decimal
|
return when (scorePreference.getOrDefault()) {
|
||||||
4 -> index.toFloat()
|
// 10 point
|
||||||
else -> throw Exception("Unknown score type")
|
POINT_10 -> IntRange(0, 10).map(Int::toString)
|
||||||
}
|
// 100 point
|
||||||
}
|
POINT_100 -> IntRange(0, 100).map(Int::toString)
|
||||||
|
// 5 stars
|
||||||
override fun displayScore(track: Track): String {
|
POINT_5 -> IntRange(0, 5).map { "$it ★" }
|
||||||
val score = track.score
|
// Smiley
|
||||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
POINT_3 -> listOf("-", "😦", "😐", "😊")
|
||||||
2 -> "${(score / 20).toInt()} ★"
|
// 10 point decimal
|
||||||
3 -> when {
|
POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
|
||||||
score == 0f -> "0"
|
else -> throw Exception("Unknown score type")
|
||||||
score <= 30 -> "😦"
|
}
|
||||||
score <= 60 -> "😐"
|
}
|
||||||
else -> "😊"
|
|
||||||
}
|
override fun indexToScore(index: Int): Float {
|
||||||
else -> track.toAnilistScore()
|
return when (scorePreference.getOrDefault()) {
|
||||||
}
|
// 10 point
|
||||||
}
|
POINT_10 -> index * 10f
|
||||||
|
// 100 point
|
||||||
override fun add(track: Track): Observable<Track> {
|
POINT_100 -> index.toFloat()
|
||||||
return api.addLibManga(track)
|
// 5 stars
|
||||||
}
|
POINT_5 -> when {
|
||||||
|
index == 0 -> 0f
|
||||||
override fun update(track: Track): Observable<Track> {
|
else -> index * 20f - 10f
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
}
|
||||||
track.status = COMPLETED
|
// Smiley
|
||||||
}
|
POINT_3 -> when {
|
||||||
|
index == 0 -> 0f
|
||||||
return api.updateLibManga(track)
|
else -> index * 25f + 10f
|
||||||
}
|
}
|
||||||
|
// 10 point decimal
|
||||||
override fun bind(track: Track): Observable<Track> {
|
POINT_10_DECIMAL -> index.toFloat()
|
||||||
return api.findLibManga(track, getUsername())
|
else -> throw Exception("Unknown score type")
|
||||||
.flatMap { remoteTrack ->
|
}
|
||||||
if (remoteTrack != null) {
|
}
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
update(track)
|
override fun displayScore(track: Track): String {
|
||||||
} else {
|
val score = track.score
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
return when (scorePreference.getOrDefault()) {
|
||||||
track.status = DEFAULT_STATUS
|
POINT_5 -> when {
|
||||||
add(track)
|
score == 0f -> "0 ★"
|
||||||
}
|
else -> "${((score + 10) / 20).toInt()} ★"
|
||||||
}
|
}
|
||||||
}
|
POINT_3 -> when {
|
||||||
|
score == 0f -> "0"
|
||||||
override fun search(query: String): Observable<List<Track>> {
|
score <= 35 -> "😦"
|
||||||
return api.search(query)
|
score <= 60 -> "😐"
|
||||||
}
|
else -> "😊"
|
||||||
|
}
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
else -> track.toAnilistScore()
|
||||||
return api.getLibManga(track, getUsername())
|
}
|
||||||
.map { remoteTrack ->
|
}
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
override fun add(track: Track): Observable<Track> {
|
||||||
track
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
override fun login(username: String, password: String) = login(password)
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
fun login(authCode: String): Completable {
|
}
|
||||||
return api.login(authCode)
|
// If user was using API v1 fetch library_id
|
||||||
// Save the token in the interceptor.
|
if (track.library_id == null || track.library_id!! == 0L){
|
||||||
.doOnNext { interceptor.setAuth(it) }
|
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
||||||
// Obtain the authenticated user from the API.
|
if (it == null) {
|
||||||
.zipWith(api.getCurrentUser().map { pair ->
|
throw Exception("$track not found on user library")
|
||||||
preferences.anilistScoreType().set(pair.second)
|
}
|
||||||
pair.first
|
track.library_id = it.library_id
|
||||||
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
|
api.updateLibManga(track)
|
||||||
// Save service credentials (username and refresh token).
|
}
|
||||||
.doOnNext { saveCredentials(it.first, it.second) }
|
}
|
||||||
// Logout on any error.
|
|
||||||
.doOnError { logout() }
|
return api.updateLibManga(track)
|
||||||
.toCompletable()
|
}
|
||||||
}
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
override fun logout() {
|
return api.findLibManga(track, getUsername().toInt())
|
||||||
super.logout()
|
.flatMap { remoteTrack ->
|
||||||
interceptor.setAuth(null)
|
if (remoteTrack != null) {
|
||||||
}
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
}
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||||
|
return api.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return api.getLibManga(track, getUsername().toInt())
|
||||||
|
.map { remoteTrack ->
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(token: String): Completable {
|
||||||
|
val oauth = api.createOAuth(token)
|
||||||
|
interceptor.setAuth(oauth)
|
||||||
|
return api.getCurrentUser().map { (username, scoreType) ->
|
||||||
|
scorePreference.set(scoreType)
|
||||||
|
saveCredentials(username.toString(), oauth.access_token)
|
||||||
|
}.doOnError{
|
||||||
|
logout()
|
||||||
|
}.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
preferences.trackToken(this).set(null)
|
||||||
|
interceptor.setAuth(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveOAuth(oAuth: OAuth?) {
|
||||||
|
preferences.trackToken(this).set(gson.toJson(oAuth))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadOAuth(): OAuth? {
|
||||||
|
return try {
|
||||||
|
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,161 +1,286 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.int
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.github.salomonbrys.kotson.string
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import okhttp3.FormBody
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.MediaType
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Response
|
import okhttp3.Request
|
||||||
import retrofit2.Retrofit
|
import okhttp3.RequestBody
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
import rx.Observable
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import java.util.Calendar
|
||||||
import retrofit2.http.*
|
|
||||||
import rx.Observable
|
|
||||||
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|
||||||
|
private val parser = JsonParser()
|
||||||
private val rest = restBuilder()
|
private val jsonMime = MediaType.parse("application/json; charset=utf-8")
|
||||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
.build()
|
|
||||||
.create(Rest::class.java)
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
|
val query = """
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||||
.map { response ->
|
| id
|
||||||
response.body()?.close()
|
| status
|
||||||
if (!response.isSuccessful) {
|
|}
|
||||||
throw Exception("Could not add manga")
|
|}
|
||||||
}
|
|""".trimMargin()
|
||||||
track
|
val variables = jsonObject(
|
||||||
}
|
"mangaId" to track.media_id,
|
||||||
}
|
"progress" to track.last_chapter_read,
|
||||||
|
"status" to track.toAnilistStatus()
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
)
|
||||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
val payload = jsonObject(
|
||||||
track.toAnilistScore())
|
"query" to query,
|
||||||
.map { response ->
|
"variables" to variables
|
||||||
response.body()?.close()
|
)
|
||||||
if (!response.isSuccessful) {
|
val body = RequestBody.create(jsonMime, payload.toString())
|
||||||
throw Exception("Could not update manga")
|
val request = Request.Builder()
|
||||||
}
|
.url(apiUrl)
|
||||||
track
|
.post(body)
|
||||||
}
|
.build()
|
||||||
}
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
fun search(query: String): Observable<List<Track>> {
|
.map { netResponse ->
|
||||||
return rest.search(query, 1)
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
.map { list ->
|
netResponse.close()
|
||||||
list.filter { it.type != "Novel" }.map { it.toTrack() }
|
if (responseBody.isEmpty()) {
|
||||||
}
|
throw Exception("Null Response")
|
||||||
.onErrorReturn { emptyList() }
|
}
|
||||||
}
|
val response = parser.parse(responseBody).obj
|
||||||
|
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
||||||
fun getList(username: String): Observable<List<Track>> {
|
track
|
||||||
return rest.getLib(username)
|
}
|
||||||
.map { lib ->
|
}
|
||||||
lib.flatten().map { it.toTrack() }
|
|
||||||
}
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
}
|
val query = """
|
||||||
|
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||||
fun findLibManga(track: Track, username: String) : Observable<Track?> {
|
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||||
// TODO avoid getting the entire list
|
|id
|
||||||
return getList(username)
|
|status
|
||||||
.map { list -> list.find { it.remote_id == track.remote_id } }
|
|progress
|
||||||
}
|
|}
|
||||||
|
|}
|
||||||
fun getLibManga(track: Track, username: String): Observable<Track> {
|
|""".trimMargin()
|
||||||
return findLibManga(track, username)
|
val variables = jsonObject(
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
"listId" to track.library_id,
|
||||||
}
|
"progress" to track.last_chapter_read,
|
||||||
|
"status" to track.toAnilistStatus(),
|
||||||
fun login(authCode: String): Observable<OAuth> {
|
"score" to track.score.toInt()
|
||||||
return restBuilder()
|
)
|
||||||
.client(client)
|
val payload = jsonObject(
|
||||||
.build()
|
"query" to query,
|
||||||
.create(Rest::class.java)
|
"variables" to variables
|
||||||
.requestAccessToken(authCode)
|
)
|
||||||
}
|
val body = RequestBody.create(jsonMime, payload.toString())
|
||||||
|
val request = Request.Builder()
|
||||||
fun getCurrentUser(): Observable<Pair<String, Int>> {
|
.url(apiUrl)
|
||||||
return rest.getCurrentUser()
|
.post(body)
|
||||||
.map { it["id"].string to it["score_type"].int }
|
.build()
|
||||||
}
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
private fun restBuilder() = Retrofit.Builder()
|
.map {
|
||||||
.baseUrl(baseUrl)
|
track
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
}
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
}
|
||||||
|
|
||||||
private interface Rest {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
|
val query = """
|
||||||
@FormUrlEncoded
|
|query Search(${'$'}query: String) {
|
||||||
@POST("auth/access_token")
|
|Page (perPage: 50) {
|
||||||
fun requestAccessToken(
|
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
@Field("code") code: String,
|
|id
|
||||||
@Field("grant_type") grant_type: String = "authorization_code",
|
|title {
|
||||||
@Field("client_id") client_id: String = clientId,
|
|romaji
|
||||||
@Field("client_secret") client_secret: String = clientSecret,
|
|}
|
||||||
@Field("redirect_uri") redirect_uri: String = clientUrl
|
|coverImage {
|
||||||
) : Observable<OAuth>
|
|large
|
||||||
|
|}
|
||||||
@GET("user")
|
|type
|
||||||
fun getCurrentUser(): Observable<JsonObject>
|
|status
|
||||||
|
|chapters
|
||||||
@GET("manga/search/{query}")
|
|description
|
||||||
fun search(
|
|startDate {
|
||||||
@Path("query") query: String,
|
|year
|
||||||
@Query("page") page: Int
|
|month
|
||||||
): Observable<List<ALManga>>
|
|day
|
||||||
|
|}
|
||||||
@GET("user/{username}/mangalist")
|
|}
|
||||||
fun getLib(
|
|}
|
||||||
@Path("username") username: String
|
|}
|
||||||
): Observable<ALUserLists>
|
|""".trimMargin()
|
||||||
|
val variables = jsonObject(
|
||||||
@FormUrlEncoded
|
"query" to search
|
||||||
@PUT("mangalist")
|
)
|
||||||
fun addLibManga(
|
val payload = jsonObject(
|
||||||
@Field("id") id: Int,
|
"query" to query,
|
||||||
@Field("chapters_read") chapters_read: Int,
|
"variables" to variables
|
||||||
@Field("list_status") list_status: String
|
)
|
||||||
) : Observable<Response<ResponseBody>>
|
val body = RequestBody.create(jsonMime, payload.toString())
|
||||||
|
val request = Request.Builder()
|
||||||
@FormUrlEncoded
|
.url(apiUrl)
|
||||||
@PUT("mangalist")
|
.post(body)
|
||||||
fun updateLibManga(
|
.build()
|
||||||
@Field("id") id: Int,
|
return authClient.newCall(request)
|
||||||
@Field("chapters_read") chapters_read: Int,
|
.asObservableSuccess()
|
||||||
@Field("list_status") list_status: String,
|
.map { netResponse ->
|
||||||
@Field("score") score_raw: String
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
) : Observable<Response<ResponseBody>>
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
|
val response = parser.parse(responseBody).obj
|
||||||
companion object {
|
val data = response["data"]!!.obj
|
||||||
private const val clientId = "tachiyomi-hrtje"
|
val page = data["Page"].obj
|
||||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
val media = page["media"].array
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
val entries = media.map { jsonToALManga(it.obj) }
|
||||||
private const val baseUrl = "https://anilist.co/api/"
|
entries.map { it.toTrack() }
|
||||||
|
}
|
||||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
}
|
||||||
.appendQueryParameter("grant_type", "authorization_code")
|
|
||||||
.appendQueryParameter("client_id", clientId)
|
|
||||||
.appendQueryParameter("redirect_uri", clientUrl)
|
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
||||||
.appendQueryParameter("response_type", "code")
|
val query = """
|
||||||
.build()
|
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||||
|
|Page {
|
||||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||||
body = FormBody.Builder()
|
|id
|
||||||
.add("grant_type", "refresh_token")
|
|status
|
||||||
.add("client_id", clientId)
|
|scoreRaw: score(format: POINT_100)
|
||||||
.add("client_secret", clientSecret)
|
|progress
|
||||||
.add("refresh_token", token)
|
|media {
|
||||||
.build())
|
|id
|
||||||
|
|title {
|
||||||
}
|
|romaji
|
||||||
|
|}
|
||||||
}
|
|coverImage {
|
||||||
|
|large
|
||||||
|
|}
|
||||||
|
|type
|
||||||
|
|status
|
||||||
|
|chapters
|
||||||
|
|description
|
||||||
|
|startDate {
|
||||||
|
|year
|
||||||
|
|month
|
||||||
|
|day
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|""".trimMargin()
|
||||||
|
val variables = jsonObject(
|
||||||
|
"id" to userid,
|
||||||
|
"manga_id" to track.media_id
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to query,
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = RequestBody.create(jsonMime, payload.toString())
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(apiUrl)
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { netResponse ->
|
||||||
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
val response = parser.parse(responseBody).obj
|
||||||
|
val data = response["data"]!!.obj
|
||||||
|
val page = data["Page"].obj
|
||||||
|
val media = page["mediaList"].array
|
||||||
|
val entries = media.map { jsonToALUserManga(it.obj) }
|
||||||
|
entries.firstOrNull()?.toTrack()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
||||||
|
return findLibManga(track, userid)
|
||||||
|
.map { it ?: throw Exception("Could not find manga") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOAuth(token: String): OAuth {
|
||||||
|
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
||||||
|
val query = """
|
||||||
|
|query User {
|
||||||
|
|Viewer {
|
||||||
|
|id
|
||||||
|
|mediaListOptions {
|
||||||
|
|scoreFormat
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|""".trimMargin()
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to query
|
||||||
|
)
|
||||||
|
val body = RequestBody.create(jsonMime, payload.toString())
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(apiUrl)
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { netResponse ->
|
||||||
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
val response = parser.parse(responseBody).obj
|
||||||
|
val data = response["data"]!!.obj
|
||||||
|
val viewer = data["Viewer"].obj
|
||||||
|
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||||
|
val date = try {
|
||||||
|
val date = Calendar.getInstance()
|
||||||
|
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
|
||||||
|
struct["startDate"]["day"].nullInt ?: 0)
|
||||||
|
date.timeInMillis
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
|
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
||||||
|
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
|
||||||
|
date, struct["chapters"].nullInt ?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
||||||
|
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "385"
|
||||||
|
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||||
|
private const val apiUrl = "https://graphql.anilist.co/"
|
||||||
|
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||||
|
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||||
|
|
||||||
|
fun mangaUrl(mediaId: Int): String {
|
||||||
|
return baseMangaUrl + mediaId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("response_type", "token")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,60 +1,58 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Response
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
*
|
*
|
||||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
* before its original expiration date.
|
* before its original expiration date.
|
||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = null
|
private var oauth: OAuth? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
if (refreshToken.isNullOrEmpty()) {
|
if (token.isNullOrEmpty()) {
|
||||||
throw Exception("Not authenticated with Anilist")
|
throw Exception("Not authenticated with Anilist")
|
||||||
}
|
}
|
||||||
|
if (oauth == null){
|
||||||
// Refresh access token if null or expired.
|
oauth = anilist.loadOAuth()
|
||||||
if (oauth == null || oauth!!.isExpired()) {
|
}
|
||||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
// Refresh access token if null or expired.
|
||||||
oauth = if (response.isSuccessful) {
|
if (oauth!!.isExpired()) {
|
||||||
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
|
anilist.logout()
|
||||||
} else {
|
throw Exception("Token expired")
|
||||||
response.close()
|
}
|
||||||
null
|
|
||||||
}
|
// Throw on null auth.
|
||||||
}
|
if (oauth == null) {
|
||||||
|
throw Exception("No authentication token")
|
||||||
// Throw on null auth.
|
}
|
||||||
if (oauth == null) {
|
|
||||||
throw Exception("Access token wasn't refreshed")
|
// Add the authorization header to the original request.
|
||||||
}
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
// Add the authorization header to the original request.
|
.build()
|
||||||
val authRequest = originalRequest.newBuilder()
|
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
return chain.proceed(authRequest)
|
||||||
.build()
|
}
|
||||||
|
|
||||||
return chain.proceed(authRequest)
|
/**
|
||||||
}
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
|
* and the oauth object.
|
||||||
/**
|
*/
|
||||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
fun setAuth(oauth: OAuth?) {
|
||||||
* and the oauth object.
|
token = oauth?.access_token
|
||||||
*/
|
this.oauth = oauth
|
||||||
fun setAuth(oauth: OAuth?) {
|
anilist.saveOAuth(oauth)
|
||||||
refreshToken = oauth?.refresh_token
|
}
|
||||||
this.oauth = oauth
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -4,68 +4,87 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val id: Int,
|
val media_id: Int,
|
||||||
val title_romaji: String,
|
val title_romaji: String,
|
||||||
|
val image_url_lge: String,
|
||||||
|
val description: String?,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
val publishing_status: String,
|
||||||
|
val start_date_fuzzy: Long,
|
||||||
val total_chapters: Int) {
|
val total_chapters: Int) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
||||||
remote_id = this@ALManga.id
|
media_id = this@ALManga.media_id
|
||||||
title = title_romaji
|
title = title_romaji
|
||||||
total_chapters = this@ALManga.total_chapters
|
total_chapters = this@ALManga.total_chapters
|
||||||
|
cover_url = image_url_lge
|
||||||
|
summary = description ?: ""
|
||||||
|
tracking_url = AnilistApi.mangaUrl(media_id)
|
||||||
|
publishing_status = this@ALManga.publishing_status
|
||||||
|
publishing_type = type
|
||||||
|
if (start_date_fuzzy != 0L) {
|
||||||
|
start_date = try {
|
||||||
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
outputDf.format(start_date_fuzzy)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ALUserManga(
|
data class ALUserManga(
|
||||||
val id: Int,
|
val library_id: Long,
|
||||||
val list_status: String,
|
val list_status: String,
|
||||||
val score_raw: Int,
|
val score_raw: Int,
|
||||||
val chapters_read: Int,
|
val chapters_read: Int,
|
||||||
val manga: ALManga) {
|
val manga: ALManga) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
remote_id = manga.id
|
media_id = manga.media_id
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = score_raw.toFloat()
|
score = score_raw.toFloat()
|
||||||
last_chapter_read = chapters_read
|
last_chapter_read = chapters_read
|
||||||
|
library_id = this@ALUserManga.library_id
|
||||||
|
total_chapters = manga.total_chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus() = when (list_status) {
|
fun toTrackStatus() = when (list_status) {
|
||||||
"reading" -> Anilist.READING
|
"CURRENT" -> Anilist.READING
|
||||||
"completed" -> Anilist.COMPLETED
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
"on-hold" -> Anilist.ON_HOLD
|
"PAUSED" -> Anilist.ON_HOLD
|
||||||
"dropped" -> Anilist.DROPPED
|
"DROPPED" -> Anilist.DROPPED
|
||||||
"plan to read" -> Anilist.PLAN_TO_READ
|
"PLANNING" -> Anilist.PLANNING
|
||||||
|
"REPEATING" -> Anilist.REPEATING
|
||||||
else -> throw NotImplementedError("Unknown status")
|
else -> throw NotImplementedError("Unknown status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
|
||||||
|
|
||||||
fun flatten() = lists.values.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Track.toAnilistStatus() = when (status) {
|
fun Track.toAnilistStatus() = when (status) {
|
||||||
Anilist.READING -> "reading"
|
Anilist.READING -> "CURRENT"
|
||||||
Anilist.COMPLETED -> "completed"
|
Anilist.COMPLETED -> "COMPLETED"
|
||||||
Anilist.ON_HOLD -> "on-hold"
|
Anilist.ON_HOLD -> "PAUSED"
|
||||||
Anilist.DROPPED -> "dropped"
|
Anilist.DROPPED -> "DROPPED"
|
||||||
Anilist.PLAN_TO_READ -> "plan to read"
|
Anilist.PLANNING -> "PLANNING"
|
||||||
|
Anilist.REPEATING -> "REPEATING"
|
||||||
else -> throw NotImplementedError("Unknown status")
|
else -> throw NotImplementedError("Unknown status")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
// 10 point
|
// 10 point
|
||||||
0 -> (score.toInt() / 10).toString()
|
"POINT_10" -> (score.toInt() / 10).toString()
|
||||||
// 100 point
|
// 100 point
|
||||||
1 -> score.toInt().toString()
|
"POINT_100" -> score.toInt().toString()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
2 -> when {
|
"POINT_5" -> when {
|
||||||
score == 0f -> "0"
|
score == 0f -> "0"
|
||||||
score < 30 -> "1"
|
score < 30 -> "1"
|
||||||
score < 50 -> "2"
|
score < 50 -> "2"
|
||||||
@ -73,14 +92,14 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrD
|
|||||||
score < 90 -> "4"
|
score < 90 -> "4"
|
||||||
else -> "5"
|
else -> "5"
|
||||||
}
|
}
|
||||||
// Smiley
|
// Smiley
|
||||||
3 -> when {
|
"POINT_3" -> when {
|
||||||
score == 0f -> "0"
|
score == 0f -> "0"
|
||||||
score <= 30 -> ":("
|
score <= 35 -> ":("
|
||||||
score <= 60 -> ":|"
|
score <= 60 -> ":|"
|
||||||
else -> ":)"
|
else -> ":)"
|
||||||
}
|
}
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
4 -> (score / 10).toString()
|
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||||
else -> throw Exception("Unknown score type")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
data class OAuth(
|
data class OAuth(
|
||||||
val access_token: String,
|
val access_token: String,
|
||||||
val token_type: String,
|
val token_type: String,
|
||||||
val expires: Long,
|
val expires: Long,
|
||||||
val expires_in: Long,
|
val expires_in: Long) {
|
||||||
val refresh_token: String?) {
|
|
||||||
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
fun isExpired() = System.currentTimeMillis() > expires
|
|
||||||
}
|
}
|