mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 11:07:51 +02:00
Compare commits
536 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
1568ac9e8a | |||
1129dacdfa | |||
fab7967018 | |||
bb40a4d6b8 | |||
90d27147e6 | |||
634247c590 | |||
fd8f7ea693 | |||
5eeb497f2b | |||
505e642691 | |||
74a7e2a17e | |||
5fec956ce6 | |||
1794782323 | |||
0210ee8747 | |||
ca412832ef | |||
1089c25b8f | |||
e85841784c | |||
ca2236958a | |||
c7686323b7 | |||
73d1a1a05e | |||
211f7b591b | |||
72ea256906 | |||
f521622d4d | |||
dc5283ce9a | |||
256a4197c9 | |||
a5a12f8b3a | |||
bbe180ecd1 | |||
67678cd49e | |||
097d4fe34c | |||
5914346ace | |||
062788f222 | |||
55be9b9ca5 | |||
0da2f91771 | |||
ff190e02d4 | |||
29fd5747eb | |||
fa8f5bc0d8 | |||
2118434823 | |||
2eeac0bf8b | |||
89b293fecd | |||
3f758d5981 | |||
e838bb43d2 | |||
b7b83305b2 | |||
8df3080e0d | |||
f88794c752 | |||
cc9e2cee1f | |||
91cb892c74 | |||
a26f908370 | |||
4d14f56fa8 | |||
d9a2255be9 | |||
5e3d71c6c5 | |||
619d94bf36 | |||
6069659e0f | |||
f6a79bde6f | |||
bb9e230b35 | |||
bc9417e16b | |||
a4313d388d | |||
4ebb3a894d | |||
0642889b64 | |||
3094d084d6 | |||
f9fec74ffd | |||
8ef3ab0d49 | |||
e9a6f8ef46 | |||
68724752f8 | |||
de8fa09366 | |||
e619870eec | |||
4be5f0dab3 | |||
abe1929b49 | |||
68c4116327 | |||
3be9881997 | |||
2e44f29882 | |||
a5520c1936 | |||
112cdd54e3 | |||
b512c67b5d | |||
d8fa7bc9d2 | |||
41397ab41d | |||
c437f1473c | |||
6020cd011d | |||
582bb3e2ca | |||
5c67161dce | |||
c00eaae62b | |||
8e4dd030d0 | |||
4d7b188999 | |||
6de260d73f | |||
2230ad59f1 | |||
e22b1661f4 | |||
bb723076ee | |||
fd9c24413d | |||
91c58640a7 | |||
c8e3375248 | |||
aeef8c02d8 | |||
08f2cd2472 | |||
fe413d52d6 | |||
add2ca0b8f | |||
ad6cdc9017 | |||
dd8cab4562 | |||
f7c791d153 | |||
b66f06d9dc | |||
89940677cc | |||
56e7a1e2a0 | |||
810f5ad531 | |||
495f5d03ac | |||
f63d2cebfc | |||
ae60e8cbd5 | |||
772bef05fc | |||
9320d1f7a4 | |||
e8912c5dc9 | |||
2b73a9d2a4 | |||
e25ce768bb | |||
05c628b33c | |||
fda0aa3ce1 | |||
cdf5bbadea | |||
c6b89a826c | |||
6264e56148 | |||
7f8bfd759f | |||
c3c2013944 | |||
1e4a01399d | |||
03967b67cf | |||
7425478a55 | |||
f807447de1 | |||
706163e7a6 | |||
a4c145c1ef | |||
c445ea90ba | |||
52c50398b8 | |||
fb89f77db7 | |||
f7b94179a4 | |||
e045ca8538 | |||
871e17c2f5 | |||
f86c3c81bf | |||
71ab6d38e4 | |||
e76fb7a524 | |||
90a99dde1f | |||
7b9f5d0e9f | |||
e4d4dbbeb6 | |||
6ef94fb59b | |||
a03dceff7d | |||
f717c57648 | |||
ca8fdad422 | |||
faa61923fb | |||
96a39f5c54 | |||
72f8c4d5e2 | |||
07cae4d684 | |||
dd56d7c0bb | |||
77d986f213 | |||
13bcefe5cd | |||
49d0e06704 | |||
2c8790c545 | |||
d0260acd3d | |||
44ec6184c8 | |||
cfa9729831 | |||
c25af3d5ad | |||
d3e9200a7f | |||
2032ba3ba3 | |||
9bcde69ee0 | |||
beca2b429c | |||
3a1699f0b3 | |||
a7192e866f | |||
dc882b4dce | |||
77b4de3941 | |||
006d17aac7 | |||
1a3a1db4ff | |||
97fa659283 | |||
f1d84ccb49 | |||
1f14240251 | |||
ea6fed6ecf | |||
d09eca7833 | |||
2c6f64c5ae | |||
ec87e4359b | |||
2b63bae989 | |||
82f4e3157a | |||
725ceab00b | |||
ba428c401d | |||
510669ee2c | |||
8d749df290 | |||
091c0c0c71 | |||
7fdd2cacd7 | |||
2241a0b2de | |||
d21a93123b | |||
e542a8d8e2 | |||
94ee4e7fb5 | |||
e3d430eb5e | |||
fd76255cf6 | |||
d180631877 | |||
1977e21363 | |||
e1a3ee1b81 | |||
cc43d9daed | |||
79705df499 | |||
36bbb906c1 | |||
816cc17ed3 | |||
97e3b5d2ab | |||
79ab9d80f2 | |||
32511149d1 | |||
cc9fd53abb | |||
4061c7450b | |||
9ad535bde6 | |||
b067096fc7 | |||
2dd58e5f7d | |||
7c42ab885b | |||
26b283d44d | |||
8c1b07c4ba | |||
f98e0858a7 | |||
8b60d5bfcb | |||
30b4c6e755 | |||
3d2a98451b | |||
aba528b227 | |||
d971768056 | |||
2e39be6625 | |||
f514d466a6 | |||
d10bf45283 | |||
a0064a1699 | |||
907472403d | |||
a9b6db9ee9 | |||
3e1dc9f400 | |||
d30c019b89 | |||
86b8712dd1 | |||
44241e03da | |||
12dcc2c31f | |||
bb89b72a81 | |||
ea790faeb3 | |||
4ef7b16925 | |||
93e244b4c4 | |||
87281d34c1 | |||
20041701cd | |||
f9c5379400 | |||
2a531f1a1e | |||
4d4b9c0d6d | |||
dc592e92b5 | |||
0db1a3167d | |||
830f792824 | |||
a13ebc3975 | |||
b28ef61618 | |||
6f297161de | |||
59c626b4a8 | |||
1d014a5a94 | |||
2dc8159d96 | |||
453f742732 | |||
5e6cf9fb02 | |||
83349fc72d | |||
979a5c8c16 | |||
9f625835ec | |||
5fd379e71b | |||
9c5b497751 | |||
4dc5f3e7d9 | |||
13954ffe01 | |||
36d4e1f7ef | |||
b716a2f8ac | |||
f98095e6cb | |||
d183aca810 | |||
52f4bddbce | |||
b837424f29 | |||
ba2a8c82f8 | |||
2856d9d6a3 | |||
71fac76e3d | |||
125f1ae34c | |||
b418169c20 | |||
f4d12ba622 | |||
c64d8c8b6b | |||
10a1ba95d6 | |||
27d3daf918 | |||
dcbd72e64d | |||
52e1e93f9d | |||
7d3d0999f3 | |||
93f90b5a62 | |||
c2b113ac0a | |||
8ff8ab4f27 | |||
414b8c9f21 | |||
4975787afa | |||
1210691fdd | |||
2a4527a8d6 | |||
2991906a85 | |||
5b1f4f189b | |||
d77a1e6925 | |||
19c713ebb2 | |||
90e0e0b72a | |||
22bbcaeed0 | |||
d7b8015df7 | |||
c1ac47e1ce | |||
e375101132 | |||
05b14bae7b | |||
eb15fe3898 | |||
4f5518bdd8 | |||
c9e1e6e020 | |||
ade73e6892 | |||
ee2aae7e3a | |||
b6011d4cf5 | |||
a31c6ff875 | |||
69baaac27e | |||
b16a90e9d9 | |||
f31aa622c0 | |||
4578edf157 | |||
33df35db1b | |||
093ddd776b | |||
da10b27219 | |||
5b4ed6f926 | |||
8fc467652d | |||
7971b64d57 | |||
9c1e2c3c45 | |||
909917e133 | |||
3b6c37a30b | |||
4a6e2a5d99 | |||
6cf84256fe | |||
876831480a | |||
aebc9a3b9e | |||
7b28614c37 | |||
4524c705da | |||
1f70be688a | |||
500eedaab7 | |||
2d2ff0a29d | |||
6d0689fe6c | |||
0b3dda18d3 | |||
09a8a494a0 | |||
11ac4df5d7 | |||
d352405ba6 | |||
a81609fd2c | |||
bf05952582 | |||
596a24fce8 | |||
9f20e40257 | |||
8be67a4431 | |||
58a2f7a874 | |||
cb92143613 | |||
08e26aa30d | |||
8e3ffe87b8 | |||
20e2bf9682 | |||
8512f97386 | |||
3ce880bc62 | |||
72ae243fa2 | |||
91829b0e7d | |||
7c3cd10696 | |||
24bdee626f | |||
6a30a75e3e | |||
ccdc336112 | |||
a4b71f4d11 | |||
c3f61e86b7 | |||
d8d93ee344 | |||
8ffff44454 | |||
568b90d0b4 | |||
46e09d174b | |||
1698a85e99 | |||
c9b62209c2 | |||
b280d6a76b | |||
29993e6412 | |||
2a5edf4547 | |||
d58c517a6c | |||
50136c319f | |||
2fb3b50535 | |||
4171e87b4b | |||
60b3036037 | |||
dfb2487640 | |||
97454ca162 | |||
4200409f79 | |||
b6a06189fb | |||
be521804c8 | |||
e95fcf6172 | |||
fbd2235a51 | |||
31b1b83606 | |||
a5d4f63281 | |||
328f9a70d3 | |||
df2b1dbeb1 | |||
f768393a4b | |||
c0a0d60c87 | |||
9cf5a4cac0 | |||
f21a030cf8 | |||
74e3d387eb | |||
8f83f497d5 | |||
6999fa858e | |||
8c1bedf796 | |||
1090c04fe3 | |||
33b04427d5 | |||
f7bb356abd | |||
e16bf0698e | |||
e6190683dd | |||
e08e41ae0d | |||
5f1a89df63 | |||
f15df40a54 | |||
a32e0e4ec5 | |||
3e8ac6b2d0 | |||
50a773f456 | |||
42484d718a | |||
81887000a8 | |||
987473df44 | |||
3680eb0bf5 | |||
3dbdc495e7 | |||
466515c801 | |||
e198f7e671 | |||
5fe1799dab | |||
ce7118084a | |||
06786322ca | |||
130b7501d1 | |||
864f001c3e | |||
1553ce973f | |||
72811e59f5 | |||
4c1da3575b | |||
05c0516a57 | |||
fe6dff9086 | |||
b6df5e6ee6 | |||
3ee5774870 | |||
c8fbb96f49 | |||
143303f7df | |||
585f7ec17d | |||
9beeca652f | |||
cd92569355 | |||
a82e1d0e45 | |||
5ad06df4ac | |||
5cfd5da338 | |||
b1d7167112 | |||
a475ecec4d | |||
5c98e020f4 | |||
eed295587d | |||
237af4b07d | |||
658860fdff | |||
21ba371a32 | |||
589160242e | |||
4de8b6e9a8 | |||
e79d536f33 | |||
9e90096328 | |||
f0a382c21a | |||
682a2c7546 | |||
2d1e85f280 | |||
dbec4fc15e | |||
95cd77e749 | |||
1f8126e2af | |||
86db7497e9 | |||
172305fc6a | |||
dad9dcd742 | |||
59b90a94d0 | |||
93fc5944f3 | |||
7039216eae | |||
7ba898f701 | |||
11262f86f9 | |||
d1db2d60ec | |||
156e43290e | |||
6687c80b2d | |||
1fbec7bf3d | |||
6196480d1d | |||
2723aeeb5c | |||
728ab18017 | |||
0714fdc7e6 | |||
7bfdfee27b | |||
10ec3a9b0c | |||
eec728f162 | |||
1ac8ef5341 | |||
18cdddf433 | |||
02a697031f | |||
6beff242b0 | |||
46cc078e93 | |||
9aa6da0642 | |||
8bd20c39aa | |||
efd36388b0 | |||
79bb207a8d | |||
0fe350af9a | |||
4e784cd7c3 | |||
12d6919421 | |||
79ec4faddb | |||
6603c0b990 | |||
b5dbdd1774 | |||
a08cea9df8 | |||
5d9c817461 | |||
f95c9a12c9 | |||
015257fe75 | |||
dd5692bb2d | |||
bc3d5dc863 | |||
a4b6003e58 | |||
8086d1db46 | |||
26f4f53ec2 | |||
6af78418a4 | |||
f629db3c10 | |||
af0cf9e52d | |||
e885469504 | |||
ca7e5260f0 | |||
dba64f849b | |||
02e43bafd6 | |||
637dda2e22 | |||
575eaee1d2 | |||
48a1e8f74c | |||
4d65038ad3 | |||
6e8a41f898 | |||
8da11dbdb9 | |||
70fabf6a6b | |||
43fafbc747 | |||
88e64c878b | |||
0ad9e4af0b | |||
a6df745daa | |||
c64bd81339 | |||
c31f0b4bb3 | |||
136136d055 | |||
c20d86e5c0 | |||
7ca99f749b | |||
7cc4405c09 | |||
62d5deaa6f | |||
7f5879ed6f | |||
a0f7761a37 | |||
ed77c60283 | |||
8f144316a6 | |||
e73eed4a9b | |||
9de3da33aa | |||
0de214c3b5 | |||
1226023dc2 | |||
5e24054a0b | |||
af2b886599 | |||
1d1e5f1f99 | |||
49628e9cf5 | |||
47bc1f7a9f | |||
f9783407bd | |||
74ffa14304 | |||
e881488bcc | |||
97ee7b81af | |||
ff6eefe1c4 | |||
9f546d13c2 | |||
2e6fc70353 | |||
270cacb1d7 |
33
.github/CONTRIBUTING.md
vendored
33
.github/CONTRIBUTING.md
vendored
@ -1,19 +1,27 @@
|
||||
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/WrBkRk4)
|
||||
3. What is your type of issue?
|
||||
* [Catalogue request](#catalogue-requests)
|
||||
* [Bugs](#bugs)
|
||||
* [Feature requests](#feature-requests)
|
||||
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||
|
||||
***
|
||||
|
||||
# Catalogue requests
|
||||
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* Include version (Setting > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Debug 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 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)
|
||||
* **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).**
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* For multipart issues use list like this:
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||
|
||||
@ -23,8 +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"
|
||||
* Include screenshot (if needed)
|
||||
|
||||
# Translations
|
||||
|
||||
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
||||
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
||||
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@ -1,7 +1 @@
|
||||
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
||||
|
||||
Remove line above and describe your issue here. Fill out version below. Use Preview.
|
||||
|
||||
|
||||
Version: r000 or v0.0.0
|
||||
(other relevant info like OS)
|
||||
|
23
.travis.yml
23
.travis.yml
@ -5,17 +5,30 @@ android:
|
||||
- tools
|
||||
|
||||
# The BuildTools version used by your project
|
||||
- build-tools-23.0.3
|
||||
- android-23
|
||||
- build-tools-25.0.1
|
||||
- android-25
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
|
||||
licenses:
|
||||
- android-sdk-license-.+
|
||||
- '.+'
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
before_script:
|
||||
- chmod +x gradlew
|
||||
- chmod +x gradlew
|
||||
|
||||
before_install:
|
||||
- mkdir "$ANDROID_HOME/licenses" || true
|
||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||
|
||||
#Build, and run tests
|
||||
script: "./gradlew clean assembleDebug testDebugUnitTest"
|
||||
script: "./gradlew clean buildStandardDebug"
|
||||
sudo: false
|
||||
|
||||
before_cache:
|
||||
@ -25,4 +38,4 @@ cache:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
env:
|
||||
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
||||
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
||||
|
11
README.md
11
README.md
@ -1,10 +1,11 @@
|
||||
| Build | Download | Auto Update |
|
||||
|-------|----------|-------------|
|
||||
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
|
||||
| Build | Download | F-Droid | Contribute | Contact |
|
||||
|-------|----------|---------|------------|---------|
|
||||
| [](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://github.com/inorichi/tachiyomi/wiki/Translation) | [](https://discord.gg/WrBkRk4) |
|
||||
|
||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||
### **Contact us on [Discord](https://discord.gg/WrBkRk4)**
|
||||
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
|
||||
|
||||
**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 is a free and open source manga reader for Android.
|
||||
|
||||
|
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
/build
|
||||
*iml
|
||||
*.iml
|
||||
.idea
|
||||
custom.gradle
|
235
app/build.gradle
235
app/build.gradle
@ -4,11 +4,15 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
if (file("custom.gradle").exists()) {
|
||||
apply from: "custom.gradle"
|
||||
}
|
||||
|
||||
ext {
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
getCommitCount = {
|
||||
return 'git rev-list --count origin/master'.execute().text.trim()
|
||||
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||
// return "1"
|
||||
}
|
||||
|
||||
@ -24,43 +28,57 @@ ext {
|
||||
}
|
||||
}
|
||||
|
||||
def includeUpdater() {
|
||||
return hasProperty("include_updater")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.3"
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
publishNonDefault true
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
targetSdkVersion 25
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode project.getProperties().get("versionCode")?.toInteger() ?: 7
|
||||
versionName "0.2.1"
|
||||
versionCode 25
|
||||
versionName "0.6.2"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
versionNameSuffix ".${getCommitCount()}"
|
||||
versionNameSuffix "-${getCommitCount()}"
|
||||
applicationIdSuffix ".debug"
|
||||
multiDexEnabled true
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
multiDexEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||
}
|
||||
fdroid {
|
||||
}
|
||||
dev {
|
||||
minSdkVersion 21
|
||||
resConfigs "en", "xxhdpi"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
exclude 'LICENSE.txt'
|
||||
@ -74,114 +92,132 @@ android {
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
}
|
||||
|
||||
kapt {
|
||||
generateStubs = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
final SUPPORT_LIBRARY_VERSION = '23.3.0'
|
||||
final DAGGER_VERSION = '2.2'
|
||||
final OKHTTP_VERSION = '3.2.0'
|
||||
final RETROFIT_VERSION = '2.0.1'
|
||||
final STORIO_VERSION = '1.8.0'
|
||||
final MOCKITO_VERSION = '1.10.19'
|
||||
|
||||
// Modified dependencies
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
|
||||
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||
compile 'com.github.inorichi:tachimage:68cd311'
|
||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// Android support library
|
||||
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
||||
final support_library_version = '25.4.0'
|
||||
compile "com.android.support:support-v4:$support_library_version"
|
||||
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||
compile "com.android.support:cardview-v7:$support_library_version"
|
||||
compile "com.android.support:design:$support_library_version"
|
||||
compile "com.android.support:recyclerview-v7:$support_library_version"
|
||||
compile "com.android.support:support-annotations:$support_library_version"
|
||||
compile "com.android.support:customtabs:$support_library_version"
|
||||
|
||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
|
||||
compile 'com.android.support:multidex:1.0.1'
|
||||
|
||||
// ReactiveX
|
||||
compile 'io.reactivex:rxandroid:1.1.0'
|
||||
compile 'io.reactivex:rxjava:1.1.1'
|
||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
||||
compile 'io.reactivex:rxandroid:1.2.1'
|
||||
compile 'io.reactivex:rxjava:1.3.0'
|
||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
|
||||
// Network client
|
||||
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
||||
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
|
||||
compile "com.squareup.okhttp3:okhttp:3.8.1"
|
||||
compile 'com.squareup.okio:okio:1.13.0'
|
||||
|
||||
// REST
|
||||
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
||||
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
|
||||
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
|
||||
|
||||
// IO
|
||||
compile 'com.squareup.okio:okio:1.7.0'
|
||||
final retrofit_version = '2.3.0'
|
||||
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
|
||||
// JSON
|
||||
compile 'com.google.code.gson:gson:2.6.2'
|
||||
compile 'com.google.code.gson:gson:2.8.1'
|
||||
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// Disk cache
|
||||
// YAML
|
||||
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||
|
||||
// JavaScript engine
|
||||
compile 'com.squareup.duktape:duktape-android:1.1.0'
|
||||
|
||||
// Disk
|
||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||
compile 'com.github.seven332:unifile:1.0.0'
|
||||
|
||||
// Parse HTML
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
// HTML parser
|
||||
compile 'org.jsoup:jsoup:1.10.2'
|
||||
|
||||
// Job scheduling
|
||||
compile 'com.evernote:android-job:1.1.11'
|
||||
compile 'com.google.android.gms:play-services-gcm:11.0.1'
|
||||
|
||||
// Changelog
|
||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
|
||||
// Database
|
||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
||||
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
||||
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
||||
compile "com.pushtorefresh.storio:sqlite:1.13.0"
|
||||
|
||||
// Model View Presenter
|
||||
compile 'info.android15.nucleus:nucleus:3.0.0-beta'
|
||||
final nucleus_version = '3.0.0'
|
||||
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
||||
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||
|
||||
// Dependency injection
|
||||
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
provided 'org.glassfish:javax.annotation:10.0-b28'
|
||||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||
|
||||
// Image library
|
||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||
compile 'com.github.bumptech.glide:glide:3.8.0'
|
||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
|
||||
// Transformations
|
||||
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||
|
||||
// Logging
|
||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||
|
||||
// Crash reports
|
||||
compile 'ch.acra:acra:4.8.5'
|
||||
compile 'ch.acra:acra:4.9.2'
|
||||
|
||||
// Sort
|
||||
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
||||
// UI
|
||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
compile 'eu.davidea:flexible-adapter:4.2.0'
|
||||
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
compile('com.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
|
||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||
|
||||
// Conductor
|
||||
compile "com.bluelinelabs:conductor:2.1.4"
|
||||
compile 'com.github.inorichi:conductor-support-preference:9e36460'
|
||||
|
||||
// RxBindings
|
||||
final rxbindings_version = '1.0.1'
|
||||
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||
|
||||
// Tests
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
|
||||
testCompile('org.robolectric:robolectric:3.0') {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
||||
}
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
final robolectric_version = '3.1.4'
|
||||
testCompile "org.robolectric:robolectric:$robolectric_version"
|
||||
testCompile "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testCompile "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
|
||||
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.0.1'
|
||||
ext.kotlin_version = '1.1.4'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@ -193,3 +229,50 @@ buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// Workaround to force a support lib version
|
||||
configurations.all {
|
||||
resolutionStrategy.eachDependency { details ->
|
||||
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
|
||||
//https://code.google.com/p/android/issues/detail?id=69224
|
||||
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 {
|
||||
android.applicationVariants.all { variant ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
app/proguard-rules.pro
vendored
111
app/proguard-rules.pro
vendored
@ -1,20 +1,24 @@
|
||||
-dontobfuscate
|
||||
|
||||
-keep class eu.kanade.tachiyomi.injection.** { *; }
|
||||
-dontwarn eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
||||
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
|
||||
# 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
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
|
||||
# Okio
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
|
||||
# Glide specific rules #
|
||||
# https://github.com/bumptech/glide
|
||||
@ -40,82 +44,47 @@
|
||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||
}
|
||||
|
||||
# Retrofit 2.X
|
||||
## https://square.github.io/retrofit/ ##
|
||||
### Support v7, Design
|
||||
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
||||
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
||||
|
||||
-dontwarn retrofit2.**
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
# AppCombat
|
||||
-keep public class android.support.v7.widget.** { *; }
|
||||
-keep public class android.support.v7.internal.widget.** { *; }
|
||||
-keep public class android.support.v7.internal.view.menu.** { *; }
|
||||
-keep public class android.support.v7.graphics.drawable.** { *; }
|
||||
|
||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
## GSON 2.2.4 specific rules ##
|
||||
-dontwarn android.support.**
|
||||
-dontwarn android.support.design.**
|
||||
-keep class android.support.design.** { *; }
|
||||
-keep interface android.support.design.** { *; }
|
||||
-keep public class android.support.design.R$* { *; }
|
||||
|
||||
|
||||
# ReactiveNetwork
|
||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||
|
||||
## GSON ##
|
||||
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
-keepattributes EnclosingMethod
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
## ACRA 4.5.0 specific rules ##
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# we need line numbers in our stack traces otherwise they are pretty useless
|
||||
-renamesourcefileattribute SourceFile
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
# SnakeYaml
|
||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||
-dontwarn org.yaml.snakeyaml.**
|
||||
|
||||
# ACRA needs "annotations" so add this...
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'.
|
||||
# Note: if you are removing log messages elsewhere in this file then this isn't necessary
|
||||
-keep class org.acra.ACRA {
|
||||
*;
|
||||
}
|
||||
|
||||
# keep this around for some enums that ACRA needs
|
||||
-keep class org.acra.ReportingInteractionMode {
|
||||
*;
|
||||
}
|
||||
|
||||
-keepnames class org.acra.sender.HttpSender$** {
|
||||
*;
|
||||
}
|
||||
|
||||
-keepnames class org.acra.ReportField {
|
||||
*;
|
||||
}
|
||||
|
||||
# keep this otherwise it is removed by ProGuard
|
||||
-keep public class org.acra.ErrorReporter {
|
||||
public void addCustomData(java.lang.String,java.lang.String);
|
||||
public void putCustomData(java.lang.String,java.lang.String);
|
||||
public void removeCustomData(java.lang.String);
|
||||
}
|
||||
|
||||
# keep this otherwise it is removed by ProGuard
|
||||
-keep public class org.acra.ErrorReporter {
|
||||
public void handleSilentException(java.lang.Throwable);
|
||||
}
|
||||
|
||||
# Keep the support library
|
||||
-keep class org.acra.** { *; }
|
||||
-keep interface org.acra.** { *; }
|
||||
# Duktape
|
||||
-keep class com.squareup.duktape.** { *; }
|
@ -1,107 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
package="eu.kanade.tachiyomi">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Tachiyomi" >
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:theme="@style/Theme.BrandedLaunch">
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.manga.MangaActivity"
|
||||
android:parentActivityName=".ui.main.MainActivity" >
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:theme="@style/Theme.Reader">
|
||||
</activity>
|
||||
android:theme="@style/Theme.Reader" />
|
||||
<activity
|
||||
android:name=".ui.setting.SettingsActivity"
|
||||
android:label="@string/label_settings"
|
||||
android:parentActivityName=".ui.main.MainActivity" >
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.category.CategoryActivity"
|
||||
android:label="@string/label_categories"
|
||||
android:parentActivityName=".ui.main.MainActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme">
|
||||
android:theme="@style/FilePickerTheme" />
|
||||
<activity
|
||||
android:name=".ui.setting.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="anilist-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".data.library.LibraryUpdateService"
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</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
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.library.LibraryUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.DownloadService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.updater.UpdateDownloaderService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupCreateService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service android:name=".data.download.DownloadService"
|
||||
<service
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver
|
||||
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".data.library.LibraryUpdateAlarm">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="eu.kanade.UPDATE_LIBRARY" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".data.updater.UpdateDownloaderAlarm">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="eu.kanade.CHECK_UPDATE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
</application>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@ -2,61 +2,67 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector
|
||||
import eu.kanade.tachiyomi.injection.component.AppComponent
|
||||
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent
|
||||
import eu.kanade.tachiyomi.injection.module.AppModule
|
||||
import android.content.res.Configuration
|
||||
import android.support.multidex.MultiDex
|
||||
import com.evernote.android.job.JobManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import org.acra.ACRA
|
||||
import org.acra.annotation.ReportsCrashes
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.InjektScope
|
||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
|
||||
@ReportsCrashes(
|
||||
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||
buildConfigClass = BuildConfig::class,
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||
)
|
||||
open class App : Application() {
|
||||
|
||||
lateinit var component: AppComponent
|
||||
private set
|
||||
|
||||
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
|
||||
private set
|
||||
|
||||
var appTheme = 0
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Injekt = InjektScope(DefaultRegistrar())
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
|
||||
component = prepareAppComponent().build()
|
||||
|
||||
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
|
||||
|
||||
setupTheme()
|
||||
setupAcra()
|
||||
setupJobManager()
|
||||
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
}
|
||||
|
||||
private fun setupTheme() {
|
||||
appTheme = PreferencesHelper.getTheme(this)
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG) {
|
||||
MultiDex.install(this)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
|
||||
return DaggerAppComponent.builder()
|
||||
.appModule(AppModule(this))
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||
}
|
||||
|
||||
protected open fun setupAcra() {
|
||||
ACRA.init(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun get(context: Context): App {
|
||||
return context.applicationContext as App
|
||||
protected open fun setupJobManager() {
|
||||
JobManager.create(this).addJobCreator { tag ->
|
||||
when (tag) {
|
||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
||||
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
||||
BackupCreatorJob.TAG -> BackupCreatorJob()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
41
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
Normal file
41
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
Normal file
@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
|
||||
addSingletonFactory { PreferencesHelper(app) }
|
||||
|
||||
addSingletonFactory { DatabaseHelper(app) }
|
||||
|
||||
addSingletonFactory { ChapterCache(app) }
|
||||
|
||||
addSingletonFactory { CoverCache(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
|
||||
addSingletonFactory { SourceManager(app) }
|
||||
|
||||
addSingletonFactory { DownloadManager(app) }
|
||||
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
}
|
||||
|
||||
}
|
10
app/src/main/java/eu/kanade/tachiyomi/Constants.kt
Normal file
10
app/src/main/java/eu/kanade/tachiyomi/Constants.kt
Normal file
@ -0,0 +1,10 @@
|
||||
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
|
||||
}
|
53
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
53
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
@ -0,0 +1,53 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
|
||||
/**
|
||||
* Performs a migration when the application is updated.
|
||||
*
|
||||
* @param preferences Preferences of the application.
|
||||
* @return true if a migration is performed, false otherwise.
|
||||
*/
|
||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||
val context = preferences.context
|
||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||
|
||||
if (oldVersion == 0) return false
|
||||
|
||||
if (oldVersion < 14) {
|
||||
// Restore jobs after upgrading to evernote's job scheduler.
|
||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||
UpdateCheckerJob.setupTask()
|
||||
}
|
||||
LibraryUpdateJob.setupTask()
|
||||
}
|
||||
if (oldVersion < 15) {
|
||||
// Delete internal chapter cache dir.
|
||||
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||
}
|
||||
if (oldVersion < 19) {
|
||||
// Move covers to external files dir.
|
||||
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
||||
if (oldDir.exists()) {
|
||||
val destDir = context.getExternalFilesDir("covers")
|
||||
if (destDir != null) {
|
||||
oldDir.listFiles().forEach {
|
||||
it.renameTo(File(destDir, it.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
|
||||
object BackupConst {
|
||||
|
||||
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.set
|
||||
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.util.sendLocalBroadcast
|
||||
import timber.log.Timber
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
/**
|
||||
* [IntentService] used to backup [Manga] information to [JsonArray]
|
||||
*/
|
||||
class BackupCreateService : IntentService(NAME) {
|
||||
|
||||
companion object {
|
||||
// Name of class
|
||||
private const val NAME = "BackupCreateService"
|
||||
|
||||
// Backup called from job
|
||||
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
||||
// Options for backup
|
||||
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||
|
||||
// Filter options
|
||||
internal const val BACKUP_CATEGORY = 0x1
|
||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||
internal const val BACKUP_CHAPTER = 0x2
|
||||
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||
internal const val BACKUP_HISTORY = 0x4
|
||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||
internal const val BACKUP_TRACK = 0x8
|
||||
internal const val BACKUP_TRACK_MASK = 0x8
|
||||
internal const val BACKUP_ALL = 0xF
|
||||
|
||||
/**
|
||||
* Make a backup from library
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of Uri
|
||||
* @param flags determines what to backup
|
||||
* @param isJob backup called from job
|
||||
*/
|
||||
fun makeBackup(context: Context, uri: Uri, flags: Int, isJob: Boolean = false) {
|
||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(EXTRA_IS_JOB, isJob)
|
||||
putExtra(EXTRA_FLAGS, flags)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val backupManager by lazy { BackupManager(this) }
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
// Get values
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||
// Create backup
|
||||
createBackupFromApp(uri, flags, isJob)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.net.Uri
|
||||
import com.evernote.android.job.Job
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.evernote.android.job.JobRequest
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class BackupCreatorJob : Job() {
|
||||
|
||||
override fun onRunJob(params: Params): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
BackupCreateService.makeBackup(context, uri, flags, true)
|
||||
return Result.SUCCESS
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "BackupCreator"
|
||||
|
||||
fun setupTask(prefInterval: Int? = null) {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
||||
if (interval > 0) {
|
||||
JobRequest.Builder(TAG)
|
||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTask() {
|
||||
JobManager.instance().cancelAllForTag(TAG)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,201 +1,212 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.*
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
||||
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_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.*
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import java.io.*
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This class provides the necessary methods to create and restore backups for the data of the
|
||||
* application. The backup follows a JSON structure, with the following scheme:
|
||||
*
|
||||
* {
|
||||
* "mangas": [
|
||||
* {
|
||||
* "manga": {"id": 1, ...},
|
||||
* "chapters": [{"id": 1, ...}, {...}],
|
||||
* "sync": [{"id": 1, ...}, {...}],
|
||||
* "categories": ["cat1", "cat2", ...]
|
||||
* },
|
||||
* { ... }
|
||||
* ],
|
||||
* "categories": [
|
||||
* {"id": 1, ...},
|
||||
* {"id": 2, ...}
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @param db the database helper.
|
||||
*/
|
||||
class BackupManager(private val db: DatabaseHelper) {
|
||||
|
||||
private val MANGA = "manga"
|
||||
private val MANGAS = "mangas"
|
||||
private val CHAPTERS = "chapters"
|
||||
private val MANGA_SYNC = "sync"
|
||||
private val CATEGORIES = "categories"
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(Integer::class.java, IntegerSerializer())
|
||||
.setExclusionStrategies(IdExclusion())
|
||||
.create()
|
||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
|
||||
/**
|
||||
* Backups the data of the application to a file.
|
||||
*
|
||||
* @param file the file where the backup will be saved.
|
||||
* @throws IOException if there's any IO error.
|
||||
* Database.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun backupToFile(file: File) {
|
||||
val root = backupToJson()
|
||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||
|
||||
FileWriter(file).use {
|
||||
gson.toJson(root, it)
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
internal val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Version of parser
|
||||
*/
|
||||
var version: Int = version
|
||||
private set
|
||||
|
||||
/**
|
||||
* Json Parser
|
||||
*/
|
||||
var parser: Gson = initParser()
|
||||
|
||||
/**
|
||||
* Preferences
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Set version of parser
|
||||
*
|
||||
* @param version version of parser
|
||||
*/
|
||||
internal fun setVersion(version: Int) {
|
||||
this.version = version
|
||||
parser = initParser()
|
||||
}
|
||||
|
||||
private fun initParser(): Gson {
|
||||
return when (version) {
|
||||
1 -> GsonBuilder().create()
|
||||
2 -> GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
.create()
|
||||
else -> throw Exception("Json version unknown")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON object containing the backup of the app's data.
|
||||
* Backup the categories of library
|
||||
*
|
||||
* @return the backup as a JSON object.
|
||||
* @param root root of categories json
|
||||
*/
|
||||
fun backupToJson(): JsonObject {
|
||||
val root = JsonObject()
|
||||
|
||||
// Backup library mangas and its dependencies
|
||||
val mangaEntries = JsonArray()
|
||||
root.add(MANGAS, mangaEntries)
|
||||
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
|
||||
mangaEntries.add(backupManga(manga))
|
||||
}
|
||||
|
||||
// Backup categories
|
||||
val categoryEntries = JsonArray()
|
||||
root.add(CATEGORIES, categoryEntries)
|
||||
for (category in db.getCategories().executeAsBlocking()) {
|
||||
categoryEntries.add(backupCategory(category))
|
||||
}
|
||||
|
||||
return root
|
||||
internal fun backupCategories(root: JsonArray) {
|
||||
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
|
||||
* Convert a manga to Json
|
||||
*
|
||||
* @param manga the manga to backup.
|
||||
* @return a JSON object containing all the data of the manga.
|
||||
* @param manga manga that gets converted
|
||||
* @return [JsonElement] containing manga information
|
||||
*/
|
||||
private fun backupManga(manga: Manga): JsonObject {
|
||||
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||
// Entry for this manga
|
||||
val entry = JsonObject()
|
||||
|
||||
// Backup manga fields
|
||||
entry.add(MANGA, gson.toJsonTree(manga))
|
||||
entry[MANGA] = parser.toJsonTree(manga)
|
||||
|
||||
// Backup all the chapters
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
if (!chapters.isEmpty()) {
|
||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
||||
}
|
||||
|
||||
// Backup manga sync
|
||||
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
|
||||
if (!mangaSync.isEmpty()) {
|
||||
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
|
||||
}
|
||||
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (!categoriesForManga.isEmpty()) {
|
||||
val categoriesNames = ArrayList<String>()
|
||||
for (category in categoriesForManga) {
|
||||
categoriesNames.add(category.name)
|
||||
// Check if user wants chapter information in backup
|
||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
if (!chapters.isEmpty()) {
|
||||
val chaptersJson = parser.toJsonTree(chapters)
|
||||
if (chaptersJson.asJsonArray.size() > 0) {
|
||||
entry[CHAPTERS] = chaptersJson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (!categoriesForManga.isEmpty()) {
|
||||
val categoriesNames = categoriesForManga.map { it.name }
|
||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
if (!tracks.isEmpty()) {
|
||||
entry[TRACK] = parser.toJsonTree(tracks)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||
if (!historyForManga.isEmpty()) {
|
||||
val historyData = historyForManga.mapNotNull { history ->
|
||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||
url?.let { DHistory(url, history.last_read) }
|
||||
}
|
||||
val historyJson = parser.toJsonTree(historyData)
|
||||
if (historyJson.asJsonArray.size() > 0) {
|
||||
entry[HISTORY] = historyJson
|
||||
}
|
||||
}
|
||||
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Backups a category.
|
||||
*
|
||||
* @param category the category to backup.
|
||||
* @return a JSON object containing the data of the category.
|
||||
*/
|
||||
private fun backupCategory(category: Category): JsonElement {
|
||||
return gson.toJsonTree(category)
|
||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||
manga.id = dbManga.id
|
||||
manga.copyFrom(dbManga)
|
||||
manga.favorite = true
|
||||
insertManga(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from a file.
|
||||
* [Observable] that fetches manga information
|
||||
*
|
||||
* @param file the file containing the backup.
|
||||
* @throws IOException if there's any IO error.
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun restoreFromFile(file: File) {
|
||||
JsonReader(FileReader(file)).use {
|
||||
val root = JsonParser().parse(it).asJsonObject
|
||||
restoreFromJson(root)
|
||||
}
|
||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = true
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from an input stream.
|
||||
* [Observable] that fetches chapter information
|
||||
*
|
||||
* @param stream the stream containing the backup.
|
||||
* @throws IOException if there's any IO error.
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun restoreFromStream(stream: InputStream) {
|
||||
JsonReader(InputStreamReader(stream)).use {
|
||||
val root = JsonParser().parse(it).asJsonObject
|
||||
restoreFromJson(root)
|
||||
}
|
||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||
.doOnNext {
|
||||
if (it.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from a JSON object. Everything executes in a single transaction so that
|
||||
* nothing is modified if there's an error.
|
||||
* Restore the categories from Json
|
||||
*
|
||||
* @param root the root of the JSON.
|
||||
* @param jsonCategories array containing categories
|
||||
*/
|
||||
fun restoreFromJson(root: JsonObject) {
|
||||
db.inTransaction {
|
||||
// Restore categories
|
||||
root.get(CATEGORIES)?.let {
|
||||
restoreCategories(it.asJsonArray)
|
||||
}
|
||||
|
||||
// Restore mangas
|
||||
root.get(MANGAS)?.let {
|
||||
restoreMangas(it.asJsonArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories.
|
||||
*
|
||||
* @param jsonCategories the categories of the json.
|
||||
*/
|
||||
private fun restoreCategories(jsonCategories: JsonArray) {
|
||||
internal fun restoreCategories(jsonCategories: JsonArray) {
|
||||
// Get categories from file and from db
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
|
||||
object : TypeToken<List<Category>>() {}.type)
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||
|
||||
// Iterate over them
|
||||
for (category in backupCategories) {
|
||||
backupCategories.forEach { category ->
|
||||
// Used to know if the category is already in the db
|
||||
var found = false
|
||||
for (dbCategory in dbCategories) {
|
||||
@ -212,106 +223,20 @@ class BackupManager(private val db: DatabaseHelper) {
|
||||
if (!found) {
|
||||
// Let the db assign the id
|
||||
category.id = null
|
||||
val result = db.insertCategory(category).executeAsBlocking()
|
||||
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||
category.id = result.insertedId()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores all the mangas and its related data.
|
||||
*
|
||||
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
||||
*/
|
||||
private fun restoreMangas(jsonMangas: JsonArray) {
|
||||
val chapterToken = object : TypeToken<List<Chapter>>() {}.type
|
||||
val mangaSyncToken = object : TypeToken<List<MangaSync>>() {}.type
|
||||
val categoriesNamesToken = object : TypeToken<List<String>>() {}.type
|
||||
|
||||
for (backupManga in jsonMangas) {
|
||||
// Map every entry to objects
|
||||
val element = backupManga.asJsonObject
|
||||
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
|
||||
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
|
||||
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
|
||||
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
|
||||
|
||||
// Restore everything related to this manga
|
||||
restoreManga(manga)
|
||||
restoreChaptersForManga(manga, chapters)
|
||||
restoreSyncForManga(manga, sync)
|
||||
restoreCategoriesForManga(manga, categories)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a manga.
|
||||
*
|
||||
* @param manga the manga to restore.
|
||||
*/
|
||||
private fun restoreManga(manga: Manga) {
|
||||
// Try to find existing manga in db
|
||||
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
if (dbManga == null) {
|
||||
// Let the db assign the id
|
||||
manga.id = null
|
||||
val result = db.insertManga(manga).executeAsBlocking()
|
||||
manga.id = result.insertedId()
|
||||
} else {
|
||||
// If it exists already, we copy only the values related to the source from the db
|
||||
// (they can be up to date). Local values (flags) are kept from the backup.
|
||||
manga.id = dbManga.id
|
||||
manga.copyFrom(dbManga)
|
||||
manga.favorite = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the chapters of a manga.
|
||||
*
|
||||
* @param manga the manga whose chapters have to be restored.
|
||||
* @param chapters the chapters to restore.
|
||||
*/
|
||||
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
for (chapter in chapters) {
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
val chaptersToUpdate = ArrayList<Chapter>()
|
||||
for (backupChapter in chapters) {
|
||||
// Try to find existing chapter in db
|
||||
val pos = dbChapters.indexOf(backupChapter)
|
||||
if (pos != -1) {
|
||||
// The chapter is already in the db, only update its fields
|
||||
val dbChapter = dbChapters[pos]
|
||||
// If one of them was read, the chapter will be marked as read
|
||||
dbChapter.read = backupChapter.read || dbChapter.read
|
||||
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
|
||||
chaptersToUpdate.add(dbChapter)
|
||||
} else {
|
||||
// Insert new chapter. Let the db assign the id
|
||||
backupChapter.id = null
|
||||
chaptersToUpdate.add(backupChapter)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!chaptersToUpdate.isEmpty()) {
|
||||
db.insertChapters(chaptersToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories a manga is in.
|
||||
*
|
||||
* @param manga the manga whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||
for (backupCategoryStr in categories) {
|
||||
for (dbCategory in dbCategories) {
|
||||
@ -326,56 +251,151 @@ class BackupManager(private val db: DatabaseHelper) {
|
||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
||||
val mangaAsList = ArrayList<Manga>()
|
||||
mangaAsList.add(manga)
|
||||
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore history from Json
|
||||
*
|
||||
* @param history list containing history to be restored
|
||||
*/
|
||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||
// List containing history to be updated
|
||||
val historyToBeUpdated = ArrayList<History>()
|
||||
for ((url, lastRead) in history) {
|
||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory.apply {
|
||||
last_read = Math.max(lastRead, dbHistory.last_read)
|
||||
}
|
||||
historyToBeUpdated.add(dbHistory)
|
||||
} else {
|
||||
// If not in database create
|
||||
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||
val historyToAdd = History.create(it).apply {
|
||||
last_read = lastRead
|
||||
}
|
||||
historyToBeUpdated.add(historyToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param manga the manga whose sync have to be restored.
|
||||
* @param sync the sync to restore.
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
for (mangaSync in sync) {
|
||||
mangaSync.manga_id = manga.id
|
||||
}
|
||||
tracks.map { it.manga_id = manga.id!! }
|
||||
|
||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
||||
val syncToUpdate = ArrayList<MangaSync>()
|
||||
for (backupSync in sync) {
|
||||
// Try to find existing chapter in db
|
||||
val pos = dbSyncs.indexOf(backupSync)
|
||||
if (pos != -1) {
|
||||
// The sync is already in the db, only update its fields
|
||||
val dbSync = dbSyncs[pos]
|
||||
// Mark the max chapter as read and nothing else
|
||||
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
|
||||
syncToUpdate.add(dbSync)
|
||||
} else {
|
||||
// Get tracks from database
|
||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
val trackToUpdate = ArrayList<Track>()
|
||||
|
||||
for (track in tracks) {
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
if (track.remote_id != dbTrack.remote_id) {
|
||||
dbTrack.remote_id = track.remote_id
|
||||
}
|
||||
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
isInDatabase = true
|
||||
trackToUpdate.add(dbTrack)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
backupSync.id = null
|
||||
syncToUpdate.add(backupSync)
|
||||
track.id = null
|
||||
trackToUpdate.add(track)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!syncToUpdate.isEmpty()) {
|
||||
db.insertMangasSync(syncToUpdate).executeAsBlocking()
|
||||
if (!trackToUpdate.isEmpty()) {
|
||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of items from a json element, or an empty list if the element is null.
|
||||
* Restore the chapters for manga if chapters already in database
|
||||
*
|
||||
* @param element the json to be mapped to a list of items.
|
||||
* @param type the gson mapping to restore the list.
|
||||
* @return a list of items.
|
||||
* @param manga manga of chapters
|
||||
* @param chapters list containing chapters that get restored
|
||||
* @return boolean answering if chapter fetch is not needed
|
||||
*/
|
||||
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
|
||||
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
|
||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
// Return if fetch is needed
|
||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
|
||||
return false
|
||||
|
||||
for (chapter in chapters) {
|
||||
val pos = dbChapters.indexOf(chapter)
|
||||
if (pos != -1) {
|
||||
val dbChapter = dbChapters[pos]
|
||||
chapter.id = dbChapter.id
|
||||
chapter.copyFrom(dbChapter)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Filter the chapters that couldn't be found.
|
||||
chapters.filter { it.id != null }
|
||||
chapters.map { it.manga_id = manga.id }
|
||||
|
||||
insertChapters(chapters)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns manga
|
||||
*
|
||||
* @return [Manga], null if not found
|
||||
*/
|
||||
internal fun getMangaFromDatabase(manga: Manga): Manga? {
|
||||
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list containing manga from library
|
||||
*
|
||||
* @return [Manga] from library
|
||||
*/
|
||||
internal fun getFavoriteManga(): List<Manga> {
|
||||
return databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
internal fun insertManga(manga: Manga): Long? {
|
||||
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts list of chapters
|
||||
*/
|
||||
internal fun insertChapters(chapters: List<Chapter>) {
|
||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of backups.
|
||||
*
|
||||
* @return number of backups selected by user
|
||||
*/
|
||||
fun numberOfBackups(): Int {
|
||||
return preferences.numberOfBackups().getOrDefault()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,406 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Restores backup from json file
|
||||
*/
|
||||
class BackupRestoreService : Service() {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return context.isServiceRunning(BackupRestoreService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a service to restore a backup from Json
|
||||
*
|
||||
* @param context context of application
|
||||
* @param uri path of Uri
|
||||
*/
|
||||
fun start(context: Context, uri: Uri) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
/**
|
||||
* Subscription where the update is done.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* The progress of a backup restore
|
||||
*/
|
||||
private var restoreProgress = 0
|
||||
|
||||
/**
|
||||
* Amount of manga in Json file (needed for restore)
|
||||
*/
|
||||
private var restoreAmount = 0
|
||||
|
||||
/**
|
||||
* List containing errors
|
||||
*/
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
/**
|
||||
* Backup manager
|
||||
*/
|
||||
private lateinit var backupManager: BackupManager
|
||||
|
||||
/**
|
||||
* Database
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
lateinit var executor: ExecutorService
|
||||
|
||||
/**
|
||||
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||
wakeLock.acquire()
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service is destroyed. It destroys the running subscription and
|
||||
* releases the wake lock.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
subscription?.unsubscribe()
|
||||
executor.shutdown() // must be called after unsubscribe
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method needs to be implemented, but it's not used/needed.
|
||||
*/
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service receives an intent.
|
||||
*
|
||||
* @param intent the start intent from.
|
||||
* @param flags the flags of the command.
|
||||
* @param startId the start id of this command.
|
||||
* @return the start value of the command.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return Service.START_NOT_STICKY
|
||||
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
|
||||
subscription = Observable.using(
|
||||
{ db.lowLevel().beginTransaction() },
|
||||
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
|
||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||
.doAfterTerminate { stopSelf(startId) }
|
||||
.subscribeOn(Schedulers.from(executor))
|
||||
.subscribe()
|
||||
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Observable] containing restore process.
|
||||
*
|
||||
* @param uri restore file
|
||||
* @return [Observable<Manga>]
|
||||
*/
|
||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
|
||||
// Restore categories
|
||||
json.get(CATEGORIES)?.let {
|
||||
backupManager.restoreCategories(it.asJsonArray)
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||
}
|
||||
|
||||
return Observable.from(mangasJson)
|
||||
.concatMap {
|
||||
val obj = it.asJsonObject
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
||||
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
||||
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
||||
|
||||
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
||||
if (observable != null) {
|
||||
observable
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
||||
restoreProgress += 1
|
||||
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
||||
Observable.just(manga)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.doOnNext {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
val logFile = writeErrorLog()
|
||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_TIME, time)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(completeIntent)
|
||||
|
||||
}
|
||||
.doOnError { error ->
|
||||
Timber.e(error)
|
||||
writeErrorLog()
|
||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
||||
}
|
||||
sendLocalBroadcast(errorIntent)
|
||||
}
|
||||
.onErrorReturn { emptyList() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Write errors to error log
|
||||
*/
|
||||
private fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
destFile.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
}
|
||||
return File("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga restore observable
|
||||
*
|
||||
* @param manga manga data from json
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
* @return [Observable] containing manga restore information
|
||||
*/
|
||||
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
||||
categories: List<String>, history: List<DHistory>,
|
||||
tracks: List<Track>): Observable<Manga>? {
|
||||
// Get source
|
||||
val source = backupManager.sourceManager.get(manga.source) ?: return null
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
|
||||
if (dbManga == null) {
|
||||
// Manga not in database
|
||||
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||
} else { // Manga in database
|
||||
// Copy information from manga already in database
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||
// Fetch rest of manga information
|
||||
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
||||
categories: List<String>, history: List<DHistory>,
|
||||
tracks: List<Track>): Observable<Manga> {
|
||||
return backupManager.restoreMangaFetchObservable(source, manga)
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
manga
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap { manga ->
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnNext {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(it, categories)
|
||||
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(it, tracks)
|
||||
}
|
||||
.doOnCompleted {
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
||||
categories: List<String>, history: List<DHistory>,
|
||||
tracks: List<Track>): Observable<Manga> {
|
||||
|
||||
return Observable.just(backupManga)
|
||||
.flatMap { manga ->
|
||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
.map { manga }
|
||||
} else {
|
||||
Observable.just(manga)
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(it, categories)
|
||||
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(it, tracks)
|
||||
}
|
||||
.doOnCompleted {
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches chapter information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called to update dialog in [BackupConst]
|
||||
*
|
||||
* @param progress restore progress
|
||||
* @param amount total restoreAmount of manga
|
||||
* @param title title of restored manga
|
||||
*/
|
||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(intent)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Json values
|
||||
*/
|
||||
object Backup {
|
||||
const val CURRENT_VERSION = 2
|
||||
const val MANGA = "manga"
|
||||
const val MANGAS = "mangas"
|
||||
const val TRACK = "track"
|
||||
const val CHAPTERS = "chapters"
|
||||
const val CATEGORIES = "categories"
|
||||
const val HISTORY = "history"
|
||||
const val VERSION = "version"
|
||||
|
||||
fun getDefaultFilename(): String {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||
return "tachiyomi_$date.json"
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
data class DHistory(val url: String,val lastRead: Long)
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||
*/
|
||||
object CategoryTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<CategoryImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.name)
|
||||
value(it.order)
|
||||
endArray()
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
val category = CategoryImpl()
|
||||
category.name = nextString()
|
||||
category.order = nextInt()
|
||||
endArray()
|
||||
category
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonToken
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||
*/
|
||||
object ChapterTypeAdapter {
|
||||
|
||||
private const val URL = "u"
|
||||
private const val READ = "r"
|
||||
private const val BOOKMARK = "b"
|
||||
private const val LAST_READ = "l"
|
||||
|
||||
fun build(): TypeAdapter<ChapterImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||
beginObject()
|
||||
name(URL)
|
||||
value(it.url)
|
||||
if (it.read) {
|
||||
name(READ)
|
||||
value(1)
|
||||
}
|
||||
if (it.bookmark) {
|
||||
name(BOOKMARK)
|
||||
value(1)
|
||||
}
|
||||
if (it.last_page_read != 0) {
|
||||
name(LAST_READ)
|
||||
value(it.last_page_read)
|
||||
}
|
||||
endObject()
|
||||
}
|
||||
}
|
||||
|
||||
read {
|
||||
val chapter = ChapterImpl()
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
if (peek() == JsonToken.NAME) {
|
||||
val name = nextName()
|
||||
|
||||
when (name) {
|
||||
URL -> chapter.url = nextString()
|
||||
READ -> chapter.read = nextInt() == 1
|
||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||
LAST_READ -> chapter.last_page_read = nextInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
endObject()
|
||||
chapter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [DHistory] to / from json
|
||||
*/
|
||||
object HistoryTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<DHistory> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
if (it.lastRead != 0L) {
|
||||
beginArray()
|
||||
value(it.url)
|
||||
value(it.lastRead)
|
||||
endArray()
|
||||
}
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
val url = nextString()
|
||||
val lastRead = nextLong()
|
||||
endArray()
|
||||
DHistory(url, lastRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.google.gson.ExclusionStrategy
|
||||
import com.google.gson.FieldAttributes
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
|
||||
class IdExclusion : ExclusionStrategy {
|
||||
|
||||
private val categoryExclusions = listOf("id")
|
||||
private val mangaExclusions = listOf("id")
|
||||
private val chapterExclusions = listOf("id", "manga_id")
|
||||
private val syncExclusions = listOf("id", "manga_id", "update")
|
||||
|
||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
||||
Manga::class.java -> mangaExclusions.contains(f.name)
|
||||
Chapter::class.java -> chapterExclusions.contains(f.name)
|
||||
MangaSync::class.java -> syncExclusions.contains(f.name)
|
||||
Category::class.java -> categoryExclusions.contains(f.name)
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun shouldSkipClass(clazz: Class<*>) = false
|
||||
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class IntegerSerializer : JsonSerializer<Int> {
|
||||
|
||||
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
|
||||
if (value != null && value !== 0)
|
||||
return JsonPrimitive(value)
|
||||
return null
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||
*/
|
||||
object MangaTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<MangaImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.url)
|
||||
value(it.title)
|
||||
value(it.source)
|
||||
value(it.viewer)
|
||||
value(it.chapter_flags)
|
||||
endArray()
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
val manga = MangaImpl()
|
||||
manga.url = nextString()
|
||||
manga.title = nextString()
|
||||
manga.source = nextLong()
|
||||
manga.viewer = nextInt()
|
||||
manga.chapter_flags = nextInt()
|
||||
endArray()
|
||||
manga
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonToken
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||
*/
|
||||
object TrackTypeAdapter {
|
||||
|
||||
private const val SYNC = "s"
|
||||
private const val REMOTE = "r"
|
||||
private const val TITLE = "t"
|
||||
private const val LAST_READ = "l"
|
||||
|
||||
fun build(): TypeAdapter<TrackImpl> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginObject()
|
||||
name(TITLE)
|
||||
value(it.title)
|
||||
name(SYNC)
|
||||
value(it.sync_id)
|
||||
name(REMOTE)
|
||||
value(it.remote_id)
|
||||
name(LAST_READ)
|
||||
value(it.last_chapter_read)
|
||||
endObject()
|
||||
}
|
||||
|
||||
read {
|
||||
val track = TrackImpl()
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
if (peek() == JsonToken.NAME) {
|
||||
val name = nextName()
|
||||
|
||||
when (name) {
|
||||
TITLE -> track.title = nextString()
|
||||
SYNC -> track.sync_id = nextInt()
|
||||
REMOTE -> track.remote_id = nextInt()
|
||||
LAST_READ -> track.last_chapter_read = nextInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
endObject()
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,18 +2,19 @@ package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.jakewharton.disklrucache.DiskLruCache
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.DiskUtils
|
||||
import eu.kanade.tachiyomi.util.saveImageTo
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.saveTo
|
||||
import okhttp3.Response
|
||||
import okio.Okio
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Class used to create chapter cache
|
||||
@ -26,15 +27,6 @@ import java.lang.reflect.Type
|
||||
*/
|
||||
class ChapterCache(private val context: Context) {
|
||||
|
||||
/** Google Json class used for parsing JSON files. */
|
||||
private val gson: Gson = Gson()
|
||||
|
||||
/** Cache class used for cache management. */
|
||||
private val diskCache: DiskLruCache
|
||||
|
||||
/** Page list collection used for deserializing from JSON. */
|
||||
private val pageListCollection: Type = object : TypeToken<List<Page>>() {}.type
|
||||
|
||||
companion object {
|
||||
/** Name of cache directory. */
|
||||
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
||||
@ -49,38 +41,41 @@ class ChapterCache(private val context: Context) {
|
||||
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
|
||||
}
|
||||
|
||||
init {
|
||||
// Open cache in default cache directory.
|
||||
diskCache = DiskLruCache.open(
|
||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
PARAMETER_CACHE_SIZE)
|
||||
}
|
||||
/** Google Json class used for parsing JSON files. */
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
/** Parent directory of the cache. Ensure not null and not root directory or fallback
|
||||
* to internal cache directory. **/
|
||||
private val basePath = context.externalCacheDir?.takeIf { it.absolutePath.length > 1 }
|
||||
?: context.cacheDir
|
||||
|
||||
/** Cache class used for cache management. */
|
||||
private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
PARAMETER_CACHE_SIZE)
|
||||
|
||||
/**
|
||||
* Returns directory of cache.
|
||||
* @return directory of cache.
|
||||
*/
|
||||
val cacheDir: File
|
||||
get() = diskCache.directory
|
||||
|
||||
/**
|
||||
* Returns real size of directory.
|
||||
* @return real size of directory.
|
||||
*/
|
||||
private val realSize: Long
|
||||
get() = DiskUtils.getDirectorySize(cacheDir)
|
||||
get() = DiskUtil.getDirectorySize(cacheDir)
|
||||
|
||||
/**
|
||||
* Returns real size of directory in human readable format.
|
||||
* @return real size of directory.
|
||||
*/
|
||||
val readableSize: String
|
||||
get() = Formatter.formatFileSize(context, realSize)
|
||||
|
||||
/**
|
||||
* Remove file from cache.
|
||||
*
|
||||
* @param file name of file "md5.0".
|
||||
* @return status of deletion for the file.
|
||||
*/
|
||||
@ -101,27 +96,29 @@ class ChapterCache(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Get page list from cache.
|
||||
* @param chapterUrl the url of the chapter.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
* @return an observable of the list of pages.
|
||||
*/
|
||||
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
|
||||
return Observable.fromCallable<List<Page>> {
|
||||
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
// Get the key for the chapter.
|
||||
val key = DiskUtils.hashKeyForDisk(chapterUrl)
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
|
||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||
diskCache.get(key).use {
|
||||
gson.fromJson(it.getString(0), pageListCollection)
|
||||
gson.fromJson<List<Page>>(it.getString(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add page list to disk cache.
|
||||
* @param chapterUrl the url of the chapter.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
* @param pages list of pages.
|
||||
*/
|
||||
fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
|
||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||
// Convert list of pages to json string.
|
||||
val cachedValue = gson.toJson(pages)
|
||||
|
||||
@ -130,7 +127,7 @@ class ChapterCache(private val context: Context) {
|
||||
|
||||
try {
|
||||
// Get editor from md5 key.
|
||||
val key = DiskUtils.hashKeyForDisk(chapterUrl)
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
editor = diskCache.edit(key) ?: return
|
||||
|
||||
// Write chapter urls to cache.
|
||||
@ -151,59 +148,61 @@ class ChapterCache(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if image is in cache.
|
||||
* Returns true if image is in cache.
|
||||
*
|
||||
* @param imageUrl url of image.
|
||||
* @return true if in cache otherwise false.
|
||||
*/
|
||||
fun isImageInCache(imageUrl: String): Boolean {
|
||||
try {
|
||||
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null
|
||||
return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image path from url.
|
||||
* Get image file from url.
|
||||
*
|
||||
* @param imageUrl url of image.
|
||||
* @return path of image.
|
||||
*/
|
||||
fun getImagePath(imageUrl: String): String? {
|
||||
try {
|
||||
// Get file from md5 key.
|
||||
val imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0"
|
||||
return File(diskCache.directory, imageName).canonicalPath
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
}
|
||||
fun getImageFile(imageUrl: String): File {
|
||||
// Get file from md5 key.
|
||||
val imageName = DiskUtil.hashKeyForDisk(imageUrl) + ".0"
|
||||
return File(diskCache.directory, imageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add image to cache.
|
||||
*
|
||||
* @param imageUrl url of image.
|
||||
* @param response http response from page.
|
||||
* @throws IOException image error.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun putImageToCache(imageUrl: String, response: Response, reencode: Boolean) {
|
||||
fun putImageToCache(imageUrl: String, response: Response) {
|
||||
// Initialize editor (edits the values for an entry).
|
||||
var editor: DiskLruCache.Editor? = null
|
||||
|
||||
try {
|
||||
// Get editor from md5 key.
|
||||
val key = DiskUtils.hashKeyForDisk(imageUrl)
|
||||
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||
|
||||
// Get OutputStream and write image with Okio.
|
||||
response.body().source().saveImageTo(editor.newOutputStream(0), reencode)
|
||||
response.body()!!.source().saveTo(editor.newOutputStream(0))
|
||||
|
||||
diskCache.flush()
|
||||
editor.commit()
|
||||
} finally {
|
||||
response.body().close()
|
||||
response.body()?.close()
|
||||
editor?.abortUnlessCommitted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKey(chapter: Chapter): String {
|
||||
return "${chapter.manga_id}${chapter.url}"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import eu.kanade.tachiyomi.util.DiskUtils
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@ -25,82 +20,21 @@ class CoverCache(private val context: Context) {
|
||||
/**
|
||||
* Cache directory used for cache management.
|
||||
*/
|
||||
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
||||
|
||||
/**
|
||||
* Download the cover with Glide and save the file.
|
||||
* @param thumbnailUrl url of thumbnail.
|
||||
* @param headers headers included in Glide request.
|
||||
* @param onReady function to call when the image is ready
|
||||
*/
|
||||
fun save(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)? = null) {
|
||||
// Check if url is empty.
|
||||
if (thumbnailUrl.isNullOrEmpty())
|
||||
return
|
||||
|
||||
// Download the cover with Glide and save the file.
|
||||
val url = GlideUrl(thumbnailUrl, headers)
|
||||
Glide.with(context)
|
||||
.load(url)
|
||||
.downloadOnly(object : SimpleTarget<File>() {
|
||||
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
|
||||
try {
|
||||
// Copy the cover from Glide's cache to local cache.
|
||||
copyToCache(thumbnailUrl!!, resource)
|
||||
|
||||
onReady?.invoke(resource)
|
||||
} catch (e: IOException) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or load the image from cache
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @param headers headers included in Glide request.
|
||||
* @param onReady function to call when the image is ready
|
||||
*/
|
||||
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)?) {
|
||||
// Check if url is empty.
|
||||
if (thumbnailUrl.isNullOrEmpty())
|
||||
return
|
||||
|
||||
// If file exist load it otherwise save it.
|
||||
val localCover = getCoverFromCache(thumbnailUrl!!)
|
||||
if (localCover.exists()) {
|
||||
onReady?.invoke(localCover)
|
||||
} else {
|
||||
save(thumbnailUrl, headers, onReady)
|
||||
}
|
||||
}
|
||||
private val cacheDir = context.getExternalFilesDir("covers")
|
||||
|
||||
/**
|
||||
* Returns the cover from cache.
|
||||
*
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @return cover image.
|
||||
*/
|
||||
private fun getCoverFromCache(thumbnailUrl: String): File {
|
||||
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the given file to this cache.
|
||||
* @param thumbnailUrl url of thumbnail.
|
||||
* @param sourceFile the source file of the cover image.
|
||||
* @throws IOException if there's any error.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
|
||||
// Get destination file.
|
||||
val destFile = getCoverFromCache(thumbnailUrl)
|
||||
|
||||
sourceFile.copyTo(destFile, overwrite = true)
|
||||
fun getCoverFile(thumbnailUrl: String): File {
|
||||
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the given stream to this cache.
|
||||
*
|
||||
* @param thumbnailUrl url of the thumbnail.
|
||||
* @param inputStream the stream to copy.
|
||||
* @throws IOException if there's any error.
|
||||
@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
|
||||
@Throws(IOException::class)
|
||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
||||
// Get destination file.
|
||||
val destFile = getCoverFromCache(thumbnailUrl)
|
||||
val destFile = getCoverFile(thumbnailUrl)
|
||||
|
||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cover file from the cache.
|
||||
*
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @return status of deletion.
|
||||
*/
|
||||
@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
|
||||
return false
|
||||
|
||||
// Remove file.
|
||||
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||
val file = getCoverFile(thumbnailUrl!!)
|
||||
return file.exists() && file.delete()
|
||||
}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
class CoverGlideModule : 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) {
|
||||
// Nothing to see here!
|
||||
}
|
||||
}
|
@ -1,315 +1,29 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Pair
|
||||
import com.pushtorefresh.storio.Queries
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.*
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.util.ChapterRecognition
|
||||
import rx.Observable
|
||||
import java.util.*
|
||||
import eu.kanade.tachiyomi.data.database.queries.*
|
||||
|
||||
open class DatabaseHelper(context: Context) {
|
||||
/**
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(context: Context)
|
||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||
|
||||
val db = DefaultStorIOSQLite.builder()
|
||||
override val db = DefaultStorIOSQLite.builder()
|
||||
.sqliteOpenHelper(DbOpenHelper(context))
|
||||
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
|
||||
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
|
||||
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
|
||||
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
|
||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||
.build()
|
||||
|
||||
inline fun inTransaction(func: DatabaseHelper.() -> Unit) {
|
||||
db.internal().beginTransaction()
|
||||
try {
|
||||
func()
|
||||
db.internal().setTransactionSuccessful()
|
||||
} finally {
|
||||
db.internal().endTransaction()
|
||||
}
|
||||
}
|
||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||
|
||||
// Mangas related queries
|
||||
|
||||
fun getMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getLibraryMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
open fun getFavoriteMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COLUMN_FAVORITE} = ?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COLUMN_TITLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getManga(url: String, sourceId: Int) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getManga(id: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COLUMN_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangasNotInLibrary() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COLUMN_FAVORITE} = ?")
|
||||
.whereArgs(0)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
|
||||
// Chapters related queries
|
||||
|
||||
fun getChapters(manga: Manga) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COLUMN_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getRecentChapters(date: Date) = db.get()
|
||||
.listOfObjects(MangaChapter::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getRecentsQuery(date))
|
||||
.observesTables(ChapterTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
|
||||
// Add a delta to the chapter number, because binary decimal representation
|
||||
// can retrieve the same chapter again
|
||||
val chapterNumber = chapter.chapter_number + 0.00001
|
||||
|
||||
return db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
|
||||
"${ChapterTable.COLUMN_CHAPTER_NUMBER} > ? AND " +
|
||||
"${ChapterTable.COLUMN_CHAPTER_NUMBER} <= ?")
|
||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||
.limit(1)
|
||||
.build())
|
||||
.prepare()
|
||||
}
|
||||
|
||||
fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
|
||||
// Add a delta to the chapter number, because binary decimal representation
|
||||
// can retrieve the same chapter again
|
||||
val chapterNumber = chapter.chapter_number - 0.00001
|
||||
|
||||
return db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder().table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
|
||||
"${ChapterTable.COLUMN_CHAPTER_NUMBER} < ? AND " +
|
||||
"${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
|
||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
|
||||
.limit(1)
|
||||
.build())
|
||||
.prepare()
|
||||
}
|
||||
|
||||
fun getNextUnreadChapter(manga: Manga) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
|
||||
"${ChapterTable.COLUMN_READ} = ? AND " +
|
||||
"${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
|
||||
.whereArgs(manga.id, 0, 0)
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||
.limit(1)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||
|
||||
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||
|
||||
// Add new chapters or delete if the source deletes them
|
||||
open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
|
||||
val dbChapters = getChapters(manga).executeAsBlocking()
|
||||
|
||||
val newChapters = Observable.from(sourceChapters)
|
||||
.filter { it !in dbChapters }
|
||||
.doOnNext { c ->
|
||||
c.manga_id = manga.id
|
||||
source.parseChapterNumber(c)
|
||||
ChapterRecognition.parseChapterNumber(c, manga)
|
||||
}.toList()
|
||||
|
||||
val deletedChapters = Observable.from(dbChapters)
|
||||
.filter { it !in sourceChapters }
|
||||
.toList()
|
||||
|
||||
return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
|
||||
var added = 0
|
||||
var deleted = 0
|
||||
var readded = 0
|
||||
|
||||
inTransaction {
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
if (!toDelete.isEmpty()) {
|
||||
for (c in toDelete) {
|
||||
if (c.read) {
|
||||
deletedReadChapterNumbers.add(c.chapter_number)
|
||||
}
|
||||
}
|
||||
deleted = deleteChapters(toDelete).executeAsBlocking().results().size
|
||||
}
|
||||
|
||||
if (!toAdd.isEmpty()) {
|
||||
// Set the date fetch for new items in reverse order to allow another sorting method.
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var now = Date().time
|
||||
|
||||
for (i in toAdd.indices.reversed()) {
|
||||
val c = toAdd[i]
|
||||
c.date_fetch = now++
|
||||
// Try to mark already read chapters as read when the source deletes them
|
||||
if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
|
||||
c.read = true
|
||||
readded++
|
||||
}
|
||||
}
|
||||
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
|
||||
}
|
||||
}
|
||||
Pair.create(added - readded, deleted - readded)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
|
||||
|
||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||
|
||||
// Manga sync related queries
|
||||
|
||||
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
||||
.`object`(MangaSync::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
|
||||
"${MangaSyncTable.COLUMN_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getMangasSync(manga: Manga) = db.get()
|
||||
.listOfObjects(MangaSync::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
// Categories related queries
|
||||
|
||||
fun getCategories() = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COLUMN_ORDER)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery(manga))
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||
|
||||
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
||||
|
||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||
|
||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||
|
||||
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
|
||||
|
||||
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
||||
|
||||
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where("${MangaCategoryTable.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
||||
inTransaction {
|
||||
deleteOldMangasCategories(mangas).executeAsBlocking()
|
||||
insertMangasCategories(mangasCategories).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
fun lowLevel() = db.lowLevel()
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
|
||||
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
||||
lowLevel().beginTransaction()
|
||||
try {
|
||||
block()
|
||||
lowLevel().setTransactionSuccessful()
|
||||
} finally {
|
||||
lowLevel().endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
||||
lowLevel().beginTransaction()
|
||||
try {
|
||||
val result = block()
|
||||
lowLevel().setTransactionSuccessful()
|
||||
return result
|
||||
} finally {
|
||||
lowLevel().endTransaction()
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import eu.kanade.tachiyomi.data.database.tables.*
|
||||
|
||||
class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DATABASE_NAME, null, DbOpenHelper.DATABASE_VERSION) {
|
||||
class DbOpenHelper(context: Context)
|
||||
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
@ -16,28 +17,47 @@ class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DA
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = 1
|
||||
const val DATABASE_VERSION = 5
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||
execSQL(MangaTable.getCreateTableQuery())
|
||||
execSQL(ChapterTable.getCreateTableQuery())
|
||||
execSQL(MangaSyncTable.getCreateTableQuery())
|
||||
execSQL(CategoryTable.getCreateTableQuery())
|
||||
execSQL(MangaCategoryTable.getCreateTableQuery())
|
||||
execSQL(MangaTable.createTableQuery)
|
||||
execSQL(ChapterTable.createTableQuery)
|
||||
execSQL(TrackTable.createTableQuery)
|
||||
execSQL(CategoryTable.createTableQuery)
|
||||
execSQL(MangaCategoryTable.createTableQuery)
|
||||
execSQL(HistoryTable.createTableQuery)
|
||||
|
||||
// DB indexes
|
||||
execSQL(MangaTable.getCreateUrlIndexQuery())
|
||||
execSQL(MangaTable.getCreateFavoriteIndexQuery())
|
||||
execSQL(ChapterTable.getCreateMangaIdIndexQuery())
|
||||
execSQL(MangaTable.createUrlIndexQuery)
|
||||
execSQL(MangaTable.createFavoriteIndexQuery)
|
||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
if (oldVersion < 2) {
|
||||
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
||||
|
||||
// Fix kissmanga covers after supporting cloudflare
|
||||
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
// Initialize history tables
|
||||
db.execSQL(HistoryTable.createTableQuery)
|
||||
db.execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
db.execSQL(ChapterTable.addScanlator)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
|
||||
interface DbProvider {
|
||||
|
||||
val db: DefaultStorIOSQLite
|
||||
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import java.util.*
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories and unread count.
|
||||
*/
|
||||
val libraryQuery =
|
||||
"SELECT M.*, COALESCE(MC.${MangaCategory.COLUMN_CATEGORY_ID}, 0) AS ${Manga.COLUMN_CATEGORY} " +
|
||||
"FROM (" +
|
||||
"SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COLUMN_UNREAD} " +
|
||||
"FROM ${Manga.TABLE} " +
|
||||
"LEFT JOIN (" +
|
||||
"SELECT ${Chapter.COLUMN_MANGA_ID}, COUNT(*) AS unread " +
|
||||
"FROM ${Chapter.TABLE} " +
|
||||
"WHERE ${Chapter.COLUMN_READ} = 0 " +
|
||||
"GROUP BY ${Chapter.COLUMN_MANGA_ID}" +
|
||||
") AS C " +
|
||||
"ON ${Manga.COLUMN_ID} = C.${Chapter.COLUMN_MANGA_ID} " +
|
||||
"WHERE ${Manga.COLUMN_FAVORITE} = 1 " +
|
||||
"GROUP BY ${Manga.COLUMN_ID} " +
|
||||
"ORDER BY ${Manga.COLUMN_TITLE}" +
|
||||
") AS M " +
|
||||
"LEFT JOIN (" +
|
||||
"SELECT * FROM ${MangaCategory.TABLE}) AS MC " +
|
||||
"ON MC.${MangaCategory.COLUMN_MANGA_ID} = M.${Manga.COLUMN_ID}"
|
||||
|
||||
/**
|
||||
* Query to get the recent chapters of manga from the library up to a date.
|
||||
*
|
||||
* @param date the delimiting date.
|
||||
*/
|
||||
fun getRecentsQuery(date: Date): String =
|
||||
"SELECT ${Manga.TABLE}.${Manga.COLUMN_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} " +
|
||||
"ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
|
||||
"WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
|
||||
"ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
|
||||
|
||||
|
||||
/**
|
||||
* Query to get the categorias for a manga.
|
||||
*
|
||||
* @param manga the manga.
|
||||
*/
|
||||
fun getCategoriesForMangaQuery(manga: MangaModel) =
|
||||
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
|
||||
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
|
||||
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
|
||||
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"
|
@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
||||
|
||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||
CategoryPutResolver(),
|
||||
CategoryGetResolver(),
|
||||
CategoryDeleteResolver()
|
||||
)
|
||||
|
||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_ORDER, obj.order)
|
||||
put(COL_FLAGS, obj.flags)
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_BOOKMARK
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||
|
||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||
ChapterPutResolver(),
|
||||
ChapterGetResolver(),
|
||||
ChapterDeleteResolver()
|
||||
)
|
||||
|
||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_READ, obj.read)
|
||||
put(COL_SCANLATOR, obj.scanlator)
|
||||
put(COL_BOOKMARK, obj.bookmark)
|
||||
put(COL_DATE_FETCH, obj.date_fetch)
|
||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
||||
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
||||
put(COL_SOURCE_ORDER, obj.source_order)
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
||||
|
||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||
HistoryPutResolver(),
|
||||
HistoryGetResolver(),
|
||||
HistoryDeleteResolver()
|
||||
)
|
||||
|
||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_CHAPTER_ID, obj.chapter_id)
|
||||
put(COL_LAST_READ, obj.last_read)
|
||||
put(COL_TIME_READ, obj.time_read)
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
||||
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
||||
|
||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||
MangaCategoryPutResolver(),
|
||||
MangaCategoryGetResolver(),
|
||||
MangaCategoryDeleteResolver()
|
||||
)
|
||||
|
||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_CATEGORY_ID, obj.category_id)
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
||||
|
||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||
MangaPutResolver(),
|
||||
MangaGetResolver(),
|
||||
MangaDeleteResolver()
|
||||
)
|
||||
|
||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_SOURCE, obj.source)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_ARTIST, obj.artist)
|
||||
put(COL_AUTHOR, obj.author)
|
||||
put(COL_DESCRIPTION, obj.description)
|
||||
put(COL_GENRE, obj.genre)
|
||||
put(COL_TITLE, obj.title)
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||
put(COL_FAVORITE, obj.favorite)
|
||||
put(COL_LAST_UPDATE, obj.last_update)
|
||||
put(COL_INITIALIZED, obj.initialized)
|
||||
put(COL_VIEWER, obj.viewer)
|
||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||
}
|
||||
}
|
||||
|
||||
open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
||||
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
||||
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||
}
|
||||
}
|
||||
|
||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||
|
||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||
TrackPutResolver(),
|
||||
TrackGetResolver(),
|
||||
TrackDeleteResolver()
|
||||
)
|
||||
|
||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Track) = ContentValues(9).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_SYNC_ID, obj.sync_id)
|
||||
put(COL_REMOTE_ID, obj.remote_id)
|
||||
put(COL_TITLE, obj.title)
|
||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_SCORE, obj.score)
|
||||
}
|
||||
}
|
||||
|
||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID))
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||
}
|
||||
}
|
||||
|
||||
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||
|
||||
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
||||
public class Category implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ID, key = true)
|
||||
public Integer id;
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
|
||||
public String name;
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
|
||||
public int order;
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
|
||||
public int flags;
|
||||
|
||||
public Category() {}
|
||||
|
||||
public static Category create(String name) {
|
||||
Category c = new Category();
|
||||
c.name = name;
|
||||
return c;
|
||||
}
|
||||
|
||||
public static Category createDefault() {
|
||||
Category c = create("Default");
|
||||
c.id = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
public String getNameLower() {
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Category category = (Category) o;
|
||||
|
||||
return name.equals(category.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Category : Serializable {
|
||||
|
||||
var id: Int?
|
||||
|
||||
var name: String
|
||||
|
||||
var order: Int
|
||||
|
||||
var flags: Int
|
||||
|
||||
val nameLower: String
|
||||
get() = name.toLowerCase()
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class CategoryImpl : Category {
|
||||
|
||||
override var id: Int? = null
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var order: Int = 0
|
||||
|
||||
override var flags: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val category = other as Category
|
||||
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||
|
||||
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
||||
public class Chapter implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_MANGA_ID)
|
||||
public Long manga_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_URL)
|
||||
public String url;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_NAME)
|
||||
public String name;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_READ)
|
||||
public boolean read;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_LAST_PAGE_READ)
|
||||
public int last_page_read;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_FETCH)
|
||||
public long date_fetch;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_UPLOAD)
|
||||
public long date_upload;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||
public float chapter_number;
|
||||
|
||||
public int status;
|
||||
|
||||
private transient List<Page> pages;
|
||||
|
||||
public Chapter() {}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = UrlUtil.getPath(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Chapter chapter = (Chapter) o;
|
||||
|
||||
return url.equals(chapter.url);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
}
|
||||
|
||||
public static Chapter create() {
|
||||
Chapter chapter = new Chapter();
|
||||
chapter.chapter_number = -1;
|
||||
return chapter;
|
||||
}
|
||||
|
||||
public List<Page> getPages() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
public void setPages(List<Page> pages) {
|
||||
this.pages = pages;
|
||||
}
|
||||
|
||||
public boolean isDownloaded() {
|
||||
return status == Download.DOWNLOADED;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import java.io.Serializable
|
||||
|
||||
interface Chapter : SChapter, Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var manga_id: Long?
|
||||
|
||||
var read: Boolean
|
||||
|
||||
var bookmark: Boolean
|
||||
|
||||
var last_page_read: Int
|
||||
|
||||
var date_fetch: Long
|
||||
|
||||
var source_order: Int
|
||||
|
||||
val isRecognizedNumber: Boolean
|
||||
get() = chapter_number >= 0f
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(): Chapter = ChapterImpl().apply {
|
||||
chapter_number = -1f
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class ChapterImpl : Chapter {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long? = null
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var scanlator: String? = null
|
||||
|
||||
override var read: Boolean = false
|
||||
|
||||
override var bookmark: Boolean = false
|
||||
|
||||
override var last_page_read: Int = 0
|
||||
|
||||
override var date_fetch: Long = 0
|
||||
|
||||
override var date_upload: Long = 0
|
||||
|
||||
override var chapter_number: Float = 0f
|
||||
|
||||
override var source_order: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val chapter = other as Chapter
|
||||
return url == chapter.url
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Object containing the history statistics of a chapter
|
||||
*/
|
||||
interface History : Serializable {
|
||||
|
||||
/**
|
||||
* Id of history object.
|
||||
*/
|
||||
var id: Long?
|
||||
|
||||
/**
|
||||
* Chapter id of history object.
|
||||
*/
|
||||
var chapter_id: Long
|
||||
|
||||
/**
|
||||
* Last time chapter was read in time long format
|
||||
*/
|
||||
var last_read: Long
|
||||
|
||||
/**
|
||||
* Total time chapter was read - todo not yet implemented
|
||||
*/
|
||||
var time_read: Long
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* History constructor
|
||||
*
|
||||
* @param chapter chapter object
|
||||
* @return history object
|
||||
*/
|
||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||
this.chapter_id = chapter.id!!
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
/**
|
||||
* Object containing the history statistics of a chapter
|
||||
*/
|
||||
class HistoryImpl : History {
|
||||
|
||||
/**
|
||||
* Id of history object.
|
||||
*/
|
||||
override var id: Long? = null
|
||||
|
||||
/**
|
||||
* Chapter id of history object.
|
||||
*/
|
||||
override var chapter_id: Long = 0
|
||||
|
||||
/**
|
||||
* Last time chapter was read in time long format
|
||||
*/
|
||||
override var last_read: Long = 0
|
||||
|
||||
/**
|
||||
* Total time chapter was read - todo not yet implemented
|
||||
*/
|
||||
override var time_read: Long = 0
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||
|
||||
@StorIOSQLiteType(table = MangaTable.TABLE)
|
||||
public class Manga implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_SOURCE)
|
||||
public int source;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_URL)
|
||||
public String url;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ARTIST)
|
||||
public String artist;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_AUTHOR)
|
||||
public String author;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_DESCRIPTION)
|
||||
public String description;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_GENRE)
|
||||
public String genre;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_TITLE)
|
||||
public String title;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_STATUS)
|
||||
public int status;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_THUMBNAIL_URL)
|
||||
public String thumbnail_url;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_FAVORITE)
|
||||
public boolean favorite;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_LAST_UPDATE)
|
||||
public long last_update;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_INITIALIZED)
|
||||
public boolean initialized;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_VIEWER)
|
||||
public int viewer;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
|
||||
public int chapter_flags;
|
||||
|
||||
public transient int unread;
|
||||
|
||||
public transient int category;
|
||||
|
||||
public static final int UNKNOWN = 0;
|
||||
public static final int ONGOING = 1;
|
||||
public static final int COMPLETED = 2;
|
||||
public static final int LICENSED = 3;
|
||||
|
||||
public static final int SORT_AZ = 0x00000000;
|
||||
public static final int SORT_ZA = 0x00000001;
|
||||
public static final int SORT_MASK = 0x00000001;
|
||||
|
||||
public static final int SHOW_UNREAD = 0x00000002;
|
||||
public static final int SHOW_READ = 0x00000004;
|
||||
public static final int READ_MASK = 0x00000006;
|
||||
|
||||
public static final int SHOW_DOWNLOADED = 0x00000008;
|
||||
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
||||
public static final int DOWNLOADED_MASK = 0x00000018;
|
||||
|
||||
// Generic filter that does not filter anything
|
||||
public static final int SHOW_ALL = 0x00000000;
|
||||
|
||||
public static final int DISPLAY_NAME = 0x00000000;
|
||||
public static final int DISPLAY_NUMBER = 0x00100000;
|
||||
public static final int DISPLAY_MASK = 0x00100000;
|
||||
|
||||
public Manga() {}
|
||||
|
||||
public static Manga create(String pathUrl) {
|
||||
Manga m = new Manga();
|
||||
m.url = pathUrl;
|
||||
return m;
|
||||
}
|
||||
|
||||
public static Manga create(String pathUrl, int source) {
|
||||
Manga m = new Manga();
|
||||
m.url = pathUrl;
|
||||
m.source = source;
|
||||
return m;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = UrlUtil.getPath(url);
|
||||
}
|
||||
|
||||
public void copyFrom(Manga other) {
|
||||
if (other.title != null)
|
||||
title = other.title;
|
||||
|
||||
if (other.author != null)
|
||||
author = other.author;
|
||||
|
||||
if (other.artist != null)
|
||||
artist = other.artist;
|
||||
|
||||
if (other.url != null)
|
||||
url = other.url;
|
||||
|
||||
if (other.description != null)
|
||||
description = other.description;
|
||||
|
||||
if (other.genre != null)
|
||||
genre = other.genre;
|
||||
|
||||
if (other.thumbnail_url != null)
|
||||
thumbnail_url = other.thumbnail_url;
|
||||
|
||||
status = other.status;
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public String getStatus(Context context) {
|
||||
switch (status) {
|
||||
case ONGOING:
|
||||
return context.getString(R.string.ongoing);
|
||||
case COMPLETED:
|
||||
return context.getString(R.string.completed);
|
||||
case LICENSED:
|
||||
return context.getString(R.string.licensed);
|
||||
default:
|
||||
return context.getString(R.string.unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public void setChapterOrder(int order) {
|
||||
setFlags(order, SORT_MASK);
|
||||
}
|
||||
|
||||
public void setDisplayMode(int mode) {
|
||||
setFlags(mode, DISPLAY_MASK);
|
||||
}
|
||||
|
||||
public void setReadFilter(int filter) {
|
||||
setFlags(filter, READ_MASK);
|
||||
}
|
||||
|
||||
public void setDownloadedFilter(int filter) {
|
||||
setFlags(filter, DOWNLOADED_MASK);
|
||||
}
|
||||
|
||||
private void setFlags(int flag, int mask) {
|
||||
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
|
||||
}
|
||||
|
||||
public boolean sortChaptersAZ() {
|
||||
return (chapter_flags & SORT_MASK) == SORT_AZ;
|
||||
}
|
||||
|
||||
// Used to display the chapter's title one way or another
|
||||
public int getDisplayMode() {
|
||||
return chapter_flags & DISPLAY_MASK;
|
||||
}
|
||||
|
||||
public int getReadFilter() {
|
||||
return chapter_flags & READ_MASK;
|
||||
}
|
||||
|
||||
public int getDownloadedFilter() {
|
||||
return chapter_flags & DOWNLOADED_MASK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Manga manga = (Manga) o;
|
||||
|
||||
return url.equals(manga.url);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
interface Manga : SManga {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var source: Long
|
||||
|
||||
var favorite: Boolean
|
||||
|
||||
var last_update: Long
|
||||
|
||||
var viewer: Int
|
||||
|
||||
var chapter_flags: Int
|
||||
|
||||
var unread: Int
|
||||
|
||||
var category: Int
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setFlags(order, SORT_MASK)
|
||||
}
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
return chapter_flags and SORT_MASK == SORT_DESC
|
||||
}
|
||||
|
||||
// Used to display the chapter's title one way or another
|
||||
var displayMode: Int
|
||||
get() = chapter_flags and DISPLAY_MASK
|
||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
||||
|
||||
var readFilter: Int
|
||||
get() = chapter_flags and READ_MASK
|
||||
set(filter) = setFlags(filter, READ_MASK)
|
||||
|
||||
var downloadedFilter: Int
|
||||
get() = chapter_flags and DOWNLOADED_MASK
|
||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
||||
|
||||
var bookmarkedFilter: Int
|
||||
get() = chapter_flags and BOOKMARKED_MASK
|
||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
||||
|
||||
var sorting: Int
|
||||
get() = chapter_flags and SORTING_MASK
|
||||
set(sort) = setFlags(sort, SORTING_MASK)
|
||||
|
||||
companion object {
|
||||
|
||||
const val SORT_DESC = 0x00000000
|
||||
const val SORT_ASC = 0x00000001
|
||||
const val SORT_MASK = 0x00000001
|
||||
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000
|
||||
|
||||
const val SHOW_UNREAD = 0x00000002
|
||||
const val SHOW_READ = 0x00000004
|
||||
const val READ_MASK = 0x00000006
|
||||
|
||||
const val SHOW_DOWNLOADED = 0x00000008
|
||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
||||
const val DOWNLOADED_MASK = 0x00000018
|
||||
|
||||
const val SHOW_BOOKMARKED = 0x00000020
|
||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
||||
const val BOOKMARKED_MASK = 0x00000060
|
||||
|
||||
const val SORTING_SOURCE = 0x00000000
|
||||
const val SORTING_NUMBER = 0x00000100
|
||||
const val SORTING_MASK = 0x00000100
|
||||
|
||||
const val DISPLAY_NAME = 0x00000000
|
||||
const val DISPLAY_NUMBER = 0x00100000
|
||||
const val DISPLAY_MASK = 0x00100000
|
||||
|
||||
fun create(source: Long): Manga = MangaImpl().apply {
|
||||
this.source = source
|
||||
}
|
||||
|
||||
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
|
||||
url = pathUrl
|
||||
this.title = title
|
||||
this.source = source
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||
|
||||
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
|
||||
public class MangaCategory {
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_MANGA_ID)
|
||||
public long manga_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_CATEGORY_ID)
|
||||
public int category_id;
|
||||
|
||||
public MangaCategory() {}
|
||||
|
||||
public static MangaCategory create(Manga manga, Category category) {
|
||||
MangaCategory mc = new MangaCategory();
|
||||
mc.manga_id = manga.id;
|
||||
mc.category_id = category.id;
|
||||
return mc;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class MangaCategory {
|
||||
|
||||
var id: Long? = null
|
||||
|
||||
var manga_id: Long = 0
|
||||
|
||||
var category_id: Int = 0
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(manga: Manga, category: Category): MangaCategory {
|
||||
val mc = MangaCategory()
|
||||
mc.manga_id = manga.id!!
|
||||
mc.category_id = category.id!!
|
||||
return mc
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
public class MangaChapter {
|
||||
|
||||
public Manga manga;
|
||||
public Chapter chapter;
|
||||
|
||||
public MangaChapter(Manga manga, Chapter chapter) {
|
||||
this.manga = manga;
|
||||
this.chapter = chapter;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class MangaChapter(val manga: Manga, val chapter: Chapter)
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
/**
|
||||
* Object containing manga, chapter and history
|
||||
*
|
||||
* @param manga object containing manga
|
||||
* @param chapter object containing chater
|
||||
* @param history object containing history
|
||||
*/
|
||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
@ -0,0 +1,53 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var source: Long = -1
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var artist: String? = null
|
||||
|
||||
override var author: String? = null
|
||||
|
||||
override var description: String? = null
|
||||
|
||||
override var genre: String? = null
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var thumbnail_url: String? = null
|
||||
|
||||
override var favorite: Boolean = false
|
||||
|
||||
override var last_update: Long = 0
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var viewer: Int = 0
|
||||
|
||||
override var chapter_flags: Int = 0
|
||||
|
||||
@Transient override var unread: Int = 0
|
||||
|
||||
@Transient override var category: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val manga = other as Manga
|
||||
|
||||
return url == manga.url
|
||||
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
}
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
|
||||
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
||||
public class MangaSync implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_MANGA_ID)
|
||||
public long manga_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SYNC_ID)
|
||||
public int sync_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_REMOTE_ID)
|
||||
public int remote_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TITLE)
|
||||
public String title;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_LAST_CHAPTER_READ)
|
||||
public int last_chapter_read;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TOTAL_CHAPTERS)
|
||||
public int total_chapters;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SCORE)
|
||||
public float score;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_STATUS)
|
||||
public int status;
|
||||
|
||||
public boolean update;
|
||||
|
||||
public static MangaSync create() {
|
||||
return new MangaSync();
|
||||
}
|
||||
|
||||
public static MangaSync create(MangaSyncService service) {
|
||||
MangaSync mangasync = new MangaSync();
|
||||
mangasync.sync_id = service.getId();
|
||||
return mangasync;
|
||||
}
|
||||
|
||||
public void copyPersonalFrom(MangaSync other) {
|
||||
last_chapter_read = other.last_chapter_read;
|
||||
score = other.score;
|
||||
status = other.status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MangaSync mangaSync = (MangaSync) o;
|
||||
|
||||
if (manga_id != mangaSync.manga_id) return false;
|
||||
if (sync_id != mangaSync.sync_id) return false;
|
||||
return remote_id == mangaSync.remote_id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (int) (manga_id ^ (manga_id >>> 32));
|
||||
result = 31 * result + sync_id;
|
||||
result = 31 * result + remote_id;
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Track : Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var manga_id: Long
|
||||
|
||||
var sync_id: Int
|
||||
|
||||
var remote_id: Int
|
||||
|
||||
var title: String
|
||||
|
||||
var last_chapter_read: Int
|
||||
|
||||
var total_chapters: Int
|
||||
|
||||
var score: Float
|
||||
|
||||
var status: Int
|
||||
|
||||
fun copyPersonalFrom(other: Track) {
|
||||
last_chapter_read = other.last_chapter_read
|
||||
score = other.score
|
||||
status = other.status
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||
sync_id = serviceId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class TrackImpl : Track {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long = 0
|
||||
|
||||
override var sync_id: Int = 0
|
||||
|
||||
override var remote_id: Int = 0
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var last_chapter_read: Int = 0
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
|
||||
override var score: Float = 0f
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
other as Track
|
||||
|
||||
if (manga_id != other.manga_id) return false
|
||||
if (sync_id != other.sync_id) return false
|
||||
return remote_id == other.remote_id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||
result = 31 * result + sync_id
|
||||
result = 31 * result + remote_id
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
|
||||
interface CategoryQueries : DbProvider {
|
||||
|
||||
fun getCategories() = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COL_ORDER)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery())
|
||||
.args(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||
|
||||
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
||||
|
||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||
|
||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import java.util.*
|
||||
|
||||
interface ChapterQueries : DbProvider {
|
||||
|
||||
fun getChapters(manga: Manga) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getRecentChapters(date: Date) = db.get()
|
||||
.listOfObjects(MangaChapter::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getRecentsQuery())
|
||||
.args(date.time)
|
||||
.observesTables(ChapterTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getChapter(id: Long) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getChapter(url: String) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(url)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
|
||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||
|
||||
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||
|
||||
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
|
||||
|
||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||
|
||||
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterBackupPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||
.`object`(chapter)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterSourceOrderPutResolver())
|
||||
.prepare()
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import java.util.*
|
||||
|
||||
interface HistoryQueries : DbProvider {
|
||||
|
||||
/**
|
||||
* Insert history into database
|
||||
* @param history object containing history information
|
||||
*/
|
||||
fun insertHistory(history: History) = db.put().`object`(history).prepare()
|
||||
|
||||
/**
|
||||
* Returns history of recent manga containing last read chapter
|
||||
* @param date recent date range
|
||||
*/
|
||||
fun getRecentManga(date: Date) = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getRecentMangasQuery())
|
||||
.args(date.time)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||
.listOfObjects(History::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getHistoryByMangaId())
|
||||
.args(mangaId)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
||||
.`object`(History::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getHistoryByChapterUrl())
|
||||
.args(chapterUrl)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
* Inserts history object if not yet in database
|
||||
* @param history history object
|
||||
*/
|
||||
fun updateHistoryLastRead(history: History) = db.put()
|
||||
.`object`(history)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
* Inserts history object if not yet in database
|
||||
* @param historyList history object list
|
||||
*/
|
||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
||||
.objects(historyList)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteHistory() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun deleteHistoryNoLastRead() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||
.whereArgs(0)
|
||||
.build())
|
||||
.prepare()
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.Queries
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.inTransaction
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
|
||||
interface MangaCategoryQueries : DbProvider {
|
||||
|
||||
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
|
||||
|
||||
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
||||
|
||||
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
||||
db.inTransaction {
|
||||
deleteOldMangasCategories(mangas).executeAsBlocking()
|
||||
insertMangasCategories(mangasCategories).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
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.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
interface MangaQueries : DbProvider {
|
||||
|
||||
fun getMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getLibraryMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getFavoriteMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COL_TITLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getManga(url: String, sourceId: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getManga(id: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun updateFlags(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateLastUpdated(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangasNotInLibrary() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(0)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun deleteMangas() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getLastReadManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getLastReadMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories and unread count.
|
||||
*/
|
||||
val libraryQuery = """
|
||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||
FROM (
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
||||
FROM ${Manga.TABLE}
|
||||
LEFT JOIN (
|
||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||
FROM ${Chapter.TABLE}
|
||||
WHERE ${Chapter.COL_READ} = 0
|
||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||
) AS C
|
||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.COL_ID}
|
||||
ORDER BY ${Manga.COL_TITLE}
|
||||
) AS M
|
||||
LEFT JOIN (
|
||||
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the recent chapters of manga from the library up to a date.
|
||||
*/
|
||||
fun getRecentsQuery() = """
|
||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the recently read chapters of manga from the library up to a date.
|
||||
* The max_last_read table contains the most recent chapters grouped by manga
|
||||
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
|
||||
* and are read after the given time period
|
||||
* @return return limit is 25
|
||||
*/
|
||||
fun getRecentMangasQuery() = """
|
||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
JOIN (
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
||||
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
||||
LIMIT 25
|
||||
"""
|
||||
|
||||
fun getHistoryByMangaId() = """
|
||||
SELECT ${History.TABLE}.*
|
||||
FROM ${History.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getHistoryByChapterUrl() = """
|
||||
SELECT ${History.TABLE}.*
|
||||
FROM ${History.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getLastReadMangaQuery() = """
|
||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
fun getTotalChapterMangaQuery()= """
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by COUNT(*)
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
fun getCategoriesForMangaQuery() = """
|
||||
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
|
||||
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||
"""
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
interface TrackQueries : DbProvider {
|
||||
|
||||
fun getTracks(manga: Manga) = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||
|
||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||
|
||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(chapter)
|
||||
val contentValues = mapToContentValues(chapter)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(chapter.url)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(chapter)
|
||||
val contentValues = mapToContentValues(chapter)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(chapter.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(chapter)
|
||||
val contentValues = mapToContentValues(chapter)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(chapter.url, chapter.manga_id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.support.annotation.NonNull
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
|
||||
class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
|
||||
/**
|
||||
* Updates last_read time of chapter
|
||||
*/
|
||||
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(history)
|
||||
|
||||
val cursor = db.lowLevel().query(Query.builder()
|
||||
.table(updateQuery.table())
|
||||
.where(updateQuery.where())
|
||||
.whereArgs(updateQuery.whereArgs())
|
||||
.build())
|
||||
|
||||
val putResult: PutResult
|
||||
|
||||
try {
|
||||
if (cursor.count == 0) {
|
||||
val insertQuery = mapToInsertQuery(history)
|
||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
|
||||
} else {
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
||||
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
putResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates update query
|
||||
* @param obj history object
|
||||
*/
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||
.whereArgs(obj.chapter_id)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Create content query
|
||||
* @param history object
|
||||
*/
|
||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
||||
class LibraryMangaGetResolver : MangaGetResolver() {
|
||||
|
||||
companion object {
|
||||
val INSTANCE = LibraryMangaGetResolver()
|
||||
@ -15,10 +14,10 @@ class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||
val manga = super.mapFromCursor(cursor)
|
||||
|
||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD)
|
||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
||||
manga.unread = cursor.getInt(unreadColumn)
|
||||
|
||||
val categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY)
|
||||
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
|
||||
manga.category = cursor.getInt(categoryColumn)
|
||||
|
||||
return manga
|
||||
|
@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
||||
|
||||
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||
|
||||
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||
val INSTANCE = MangaChapterGetResolver()
|
||||
}
|
||||
|
||||
private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
|
||||
private val mangaGetResolver = MangaGetResolver()
|
||||
|
||||
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver()
|
||||
private val chapterGetResolver = ChapterGetResolver()
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||
manga.id = chapter.manga_id
|
||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"));
|
||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||
|
||||
return MangaChapter(manga, chapter)
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
|
||||
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
|
||||
companion object {
|
||||
val INSTANCE = MangaChapterHistoryGetResolver()
|
||||
}
|
||||
|
||||
/**
|
||||
* Manga get resolver
|
||||
*/
|
||||
private val mangaGetResolver = MangaGetResolver()
|
||||
|
||||
/**
|
||||
* Chapter get resolver
|
||||
*/
|
||||
private val chapterResolver = ChapterGetResolver()
|
||||
|
||||
/**
|
||||
* History get resolver
|
||||
*/
|
||||
private val historyGetResolver = HistoryGetResolver()
|
||||
|
||||
/**
|
||||
* Map correct objects from cursor result
|
||||
*/
|
||||
override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
|
||||
// Get manga object
|
||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||
|
||||
// Get chapter object
|
||||
val chapter = chapterResolver.mapFromCursor(cursor)
|
||||
|
||||
// Get history object
|
||||
val history = historyGetResolver.mapFromCursor(cursor)
|
||||
|
||||
// Make certain column conflicts are dealt with
|
||||
manga.id = chapter.manga_id
|
||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||
chapter.id = history.chapter_id
|
||||
|
||||
// Return result
|
||||
return MangaChapterHistory(manga, chapter, history)
|
||||
}
|
||||
}
|
@ -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 MangaFlagsPutResolver : 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_CHAPTER_FLAGS, manga.chapter_flags)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class CategoryTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "categories";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_NAME = "name";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ORDER = "sort";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_FLAGS = "flags";
|
||||
|
||||
// This is just class with Meta Data, we don't need instances
|
||||
private CategoryTable() {
|
||||
throw new IllegalStateException("No instances please");
|
||||
}
|
||||
|
||||
// Better than static final field -> allows VM to unload useless String
|
||||
// Because you need this string only once per application life on the device
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_NAME + " TEXT NOT NULL, "
|
||||
+ COLUMN_ORDER + " INTEGER NOT NULL, "
|
||||
+ COLUMN_FLAGS + " INTEGER NOT NULL"
|
||||
+ ");";
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object CategoryTable {
|
||||
|
||||
const val TABLE = "categories"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_NAME = "name"
|
||||
|
||||
const val COL_ORDER = "sort"
|
||||
|
||||
const val COL_FLAGS = "flags"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_NAME TEXT NOT NULL,
|
||||
$COL_ORDER INTEGER NOT NULL,
|
||||
$COL_FLAGS INTEGER NOT NULL
|
||||
)"""
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class ChapterTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "chapters";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_URL = "url";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_NAME = "name";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_READ = "read";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_DATE_FETCH = "date_fetch";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_DATE_UPLOAD = "date_upload";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_LAST_PAGE_READ = "last_page_read";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CHAPTER_NUMBER = "chapter_number";
|
||||
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_URL + " TEXT NOT NULL, "
|
||||
+ COLUMN_NAME + " TEXT NOT NULL, "
|
||||
+ COLUMN_READ + " BOOLEAN NOT NULL, "
|
||||
+ COLUMN_LAST_PAGE_READ + " INT NOT NULL, "
|
||||
+ COLUMN_CHAPTER_NUMBER + " FLOAT NOT NULL, "
|
||||
+ COLUMN_DATE_FETCH + " LONG NOT NULL, "
|
||||
+ COLUMN_DATE_UPLOAD + " LONG NOT NULL, "
|
||||
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE"
|
||||
+ ");";
|
||||
}
|
||||
|
||||
public static String getCreateMangaIdIndexQuery() {
|
||||
return "CREATE INDEX " + TABLE + "_" + COLUMN_MANGA_ID + "_index ON " + TABLE + "(" + COLUMN_MANGA_ID + ");";
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object ChapterTable {
|
||||
|
||||
const val TABLE = "chapters"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_URL = "url"
|
||||
|
||||
const val COL_NAME = "name"
|
||||
|
||||
const val COL_READ = "read"
|
||||
|
||||
const val COL_SCANLATOR = "scanlator"
|
||||
|
||||
const val COL_BOOKMARK = "bookmark"
|
||||
|
||||
const val COL_DATE_FETCH = "date_fetch"
|
||||
|
||||
const val COL_DATE_UPLOAD = "date_upload"
|
||||
|
||||
const val COL_LAST_PAGE_READ = "last_page_read"
|
||||
|
||||
const val COL_CHAPTER_NUMBER = "chapter_number"
|
||||
|
||||
const val COL_SOURCE_ORDER = "source_order"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_URL TEXT NOT NULL,
|
||||
$COL_NAME TEXT NOT NULL,
|
||||
$COL_SCANLATOR TEXT,
|
||||
$COL_READ BOOLEAN NOT NULL,
|
||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
||||
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
||||
$COL_DATE_FETCH LONG NOT NULL,
|
||||
$COL_DATE_UPLOAD LONG NOT NULL,
|
||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
val createMangaIdIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
|
||||
|
||||
val sourceOrderUpdateQuery: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
||||
|
||||
val bookmarkUpdateQuery: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
||||
|
||||
val addScanlator: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object HistoryTable {
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
const val TABLE = "history"
|
||||
|
||||
/**
|
||||
* Id column name
|
||||
*/
|
||||
const val COL_ID = "${TABLE}_id"
|
||||
|
||||
/**
|
||||
* Chapter id column name
|
||||
*/
|
||||
const val COL_CHAPTER_ID = "${TABLE}_chapter_id"
|
||||
|
||||
/**
|
||||
* Last read column name
|
||||
*/
|
||||
const val COL_LAST_READ = "${TABLE}_last_read"
|
||||
|
||||
/**
|
||||
* Time read column name
|
||||
*/
|
||||
const val COL_TIME_READ = "${TABLE}_time_read"
|
||||
|
||||
/**
|
||||
* query to create history table
|
||||
*/
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
||||
$COL_LAST_READ LONG,
|
||||
$COL_TIME_READ LONG,
|
||||
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
/**
|
||||
* query to index history chapter id
|
||||
*/
|
||||
val createChapterIdIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class MangaCategoryTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "mangas_categories";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CATEGORY_ID = "category_id";
|
||||
|
||||
// This is just class with Meta Data, we don't need instances
|
||||
private MangaCategoryTable() {
|
||||
throw new IllegalStateException("No instances please");
|
||||
}
|
||||
|
||||
// Better than static final field -> allows VM to unload useless String
|
||||
// Because you need this string only once per application life on the device
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_CATEGORY_ID + " INTEGER NOT NULL, "
|
||||
+ "FOREIGN KEY(" + COLUMN_CATEGORY_ID + ") REFERENCES " + CategoryTable.TABLE + "(" + CategoryTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE, "
|
||||
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE"
|
||||
+ ");";
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MangaCategoryTable {
|
||||
|
||||
const val TABLE = "mangas_categories"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_CATEGORY_ID = "category_id"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_CATEGORY_ID INTEGER NOT NULL,
|
||||
FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID})
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class MangaSyncTable {
|
||||
|
||||
public static final String TABLE = "manga_sync";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||
|
||||
public static final String COLUMN_SYNC_ID = "sync_id";
|
||||
|
||||
public static final String COLUMN_REMOTE_ID = "remote_id";
|
||||
|
||||
public static final String COLUMN_TITLE = "title";
|
||||
|
||||
public static final String COLUMN_LAST_CHAPTER_READ = "last_chapter_read";
|
||||
|
||||
public static final String COLUMN_STATUS = "status";
|
||||
|
||||
public static final String COLUMN_SCORE = "score";
|
||||
|
||||
public static final String COLUMN_TOTAL_CHAPTERS = "total_chapters";
|
||||
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_SYNC_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_REMOTE_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_TITLE + " TEXT NOT NULL, "
|
||||
+ COLUMN_LAST_CHAPTER_READ + " INTEGER NOT NULL, "
|
||||
+ COLUMN_TOTAL_CHAPTERS + " INTEGER NOT NULL, "
|
||||
+ COLUMN_STATUS + " INTEGER NOT NULL, "
|
||||
+ COLUMN_SCORE + " FLOAT NOT NULL, "
|
||||
+ "UNIQUE (" + COLUMN_MANGA_ID + ", " + COLUMN_SYNC_ID + ") ON CONFLICT REPLACE, "
|
||||
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE"
|
||||
+ ");";
|
||||
}
|
||||
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class MangaTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "mangas";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_SOURCE = "source";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_URL = "url";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ARTIST = "artist";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_AUTHOR = "author" ;
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_DESCRIPTION = "description";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_GENRE = "genre";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_TITLE = "title";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_STATUS = "status";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_THUMBNAIL_URL = "thumbnail_url";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_FAVORITE = "favorite";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_LAST_UPDATE = "last_update";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_INITIALIZED = "initialized";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_VIEWER = "viewer";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CHAPTER_FLAGS = "chapter_flags";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_UNREAD = "unread";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CATEGORY = "category";
|
||||
|
||||
// This is just class with Meta Data, we don't need instances
|
||||
private MangaTable() {
|
||||
throw new IllegalStateException("No instances please");
|
||||
}
|
||||
|
||||
// Better than static final field -> allows VM to unload useless String
|
||||
// Because you need this string only once per application life on the device
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_SOURCE + " INTEGER NOT NULL, "
|
||||
+ COLUMN_URL + " TEXT NOT NULL, "
|
||||
+ COLUMN_ARTIST + " TEXT, "
|
||||
+ COLUMN_AUTHOR + " TEXT, "
|
||||
+ COLUMN_DESCRIPTION + " TEXT, "
|
||||
+ COLUMN_GENRE + " TEXT, "
|
||||
+ COLUMN_TITLE + " TEXT NOT NULL, "
|
||||
+ COLUMN_STATUS + " INTEGER NOT NULL, "
|
||||
+ COLUMN_THUMBNAIL_URL + " TEXT, "
|
||||
+ COLUMN_FAVORITE + " INTEGER NOT NULL, "
|
||||
+ COLUMN_LAST_UPDATE + " LONG, "
|
||||
+ COLUMN_INITIALIZED + " BOOLEAN NOT NULL, "
|
||||
+ COLUMN_VIEWER + " INTEGER NOT NULL, "
|
||||
+ COLUMN_CHAPTER_FLAGS + " INTEGER NOT NULL"
|
||||
+ ");";
|
||||
|
||||
}
|
||||
|
||||
public static String getCreateUrlIndexQuery() {
|
||||
return "CREATE INDEX " + TABLE + "_" + COLUMN_URL + "_index ON " + TABLE + "(" + COLUMN_URL + ");";
|
||||
}
|
||||
|
||||
public static String getCreateFavoriteIndexQuery() {
|
||||
return "CREATE INDEX " + TABLE + "_" + COLUMN_FAVORITE + "_index ON " + TABLE + "(" + COLUMN_FAVORITE + ");";
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MangaTable {
|
||||
|
||||
const val TABLE = "mangas"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_SOURCE = "source"
|
||||
|
||||
const val COL_URL = "url"
|
||||
|
||||
const val COL_ARTIST = "artist"
|
||||
|
||||
const val COL_AUTHOR = "author"
|
||||
|
||||
const val COL_DESCRIPTION = "description"
|
||||
|
||||
const val COL_GENRE = "genre"
|
||||
|
||||
const val COL_TITLE = "title"
|
||||
|
||||
const val COL_STATUS = "status"
|
||||
|
||||
const val COL_THUMBNAIL_URL = "thumbnail_url"
|
||||
|
||||
const val COL_FAVORITE = "favorite"
|
||||
|
||||
const val COL_LAST_UPDATE = "last_update"
|
||||
|
||||
const val COL_INITIALIZED = "initialized"
|
||||
|
||||
const val COL_VIEWER = "viewer"
|
||||
|
||||
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
||||
|
||||
const val COL_UNREAD = "unread"
|
||||
|
||||
const val COL_CATEGORY = "category"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_SOURCE INTEGER NOT NULL,
|
||||
$COL_URL TEXT NOT NULL,
|
||||
$COL_ARTIST TEXT,
|
||||
$COL_AUTHOR TEXT,
|
||||
$COL_DESCRIPTION TEXT,
|
||||
$COL_GENRE TEXT,
|
||||
$COL_TITLE TEXT NOT NULL,
|
||||
$COL_STATUS INTEGER NOT NULL,
|
||||
$COL_THUMBNAIL_URL TEXT,
|
||||
$COL_FAVORITE INTEGER NOT NULL,
|
||||
$COL_LAST_UPDATE LONG,
|
||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||
$COL_VIEWER INTEGER NOT NULL,
|
||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL
|
||||
)"""
|
||||
|
||||
val createUrlIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
|
||||
|
||||
val createFavoriteIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE)"
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object TrackTable {
|
||||
|
||||
const val TABLE = "manga_sync"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_SYNC_ID = "sync_id"
|
||||
|
||||
const val COL_REMOTE_ID = "remote_id"
|
||||
|
||||
const val COL_TITLE = "title"
|
||||
|
||||
const val COL_LAST_CHAPTER_READ = "last_chapter_read"
|
||||
|
||||
const val COL_STATUS = "status"
|
||||
|
||||
const val COL_SCORE = "score"
|
||||
|
||||
const val COL_TOTAL_CHAPTERS = "total_chapters"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_SYNC_ID INTEGER NOT NULL,
|
||||
$COL_REMOTE_ID INTEGER NOT NULL,
|
||||
$COL_TITLE TEXT NOT NULL,
|
||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||
$COL_STATUS INTEGER NOT NULL,
|
||||
$COL_SCORE FLOAT NOT NULL,
|
||||
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
}
|
@ -1,413 +1,180 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.BehaviorSubject
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
|
||||
/**
|
||||
* This class is used to manage chapter downloads in the application. It must be instantiated once
|
||||
* and retrieved through dependency injection. You can use this class to queue new chapters or query
|
||||
* downloaded chapters.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(context: Context) {
|
||||
|
||||
private val gson = Gson()
|
||||
/**
|
||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||
*/
|
||||
private val provider = DownloadProvider(context)
|
||||
|
||||
private val downloadsQueueSubject = PublishSubject.create<List<Download>>()
|
||||
val runningSubject = BehaviorSubject.create<Boolean>()
|
||||
private var downloadsSubscription: Subscription? = null
|
||||
/**
|
||||
* Downloader whose only task is to download chapters.
|
||||
*/
|
||||
private val downloader = Downloader(context, provider)
|
||||
|
||||
private val threadsSubject = BehaviorSubject.create<Int>()
|
||||
private var threadsSubscription: Subscription? = null
|
||||
/**
|
||||
* Downloads queue, where the pending chapters are stored.
|
||||
*/
|
||||
val queue: DownloadQueue
|
||||
get() = downloader.queue
|
||||
|
||||
val queue = DownloadQueue()
|
||||
|
||||
val imageFilenameRegex = "[^\\sa-zA-Z0-9.-]".toRegex()
|
||||
|
||||
val PAGE_LIST_FILE = "index.json"
|
||||
|
||||
@Volatile private var isRunning: Boolean = false
|
||||
|
||||
private fun initializeSubscriptions() {
|
||||
downloadsSubscription?.unsubscribe()
|
||||
|
||||
threadsSubscription = preferences.downloadThreads().asObservable()
|
||||
.subscribe { threadsSubject.onNext(it) }
|
||||
|
||||
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
|
||||
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.map { download -> areAllDownloadsFinished() }
|
||||
.subscribe({ finished ->
|
||||
if (finished!!) {
|
||||
DownloadService.stop(context)
|
||||
}
|
||||
}, { e ->
|
||||
DownloadService.stop(context)
|
||||
Timber.e(e, e.message)
|
||||
context.toast(e.message)
|
||||
})
|
||||
|
||||
if (!isRunning) {
|
||||
isRunning = true
|
||||
runningSubject.onNext(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun destroySubscriptions() {
|
||||
if (isRunning) {
|
||||
isRunning = false
|
||||
runningSubject.onNext(false)
|
||||
}
|
||||
|
||||
if (downloadsSubscription != null) {
|
||||
downloadsSubscription?.unsubscribe()
|
||||
downloadsSubscription = null
|
||||
}
|
||||
|
||||
if (threadsSubscription != null) {
|
||||
threadsSubscription?.unsubscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create a download object for every chapter in the event and add them to the downloads queue
|
||||
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
|
||||
val manga = event.manga
|
||||
val source = sourceManager.get(manga.source)
|
||||
|
||||
// Used to avoid downloading chapters with the same name
|
||||
val addedChapters = ArrayList<String>()
|
||||
val pending = ArrayList<Download>()
|
||||
|
||||
for (chapter in event.chapters) {
|
||||
if (addedChapters.contains(chapter.name))
|
||||
continue
|
||||
|
||||
addedChapters.add(chapter.name)
|
||||
val download = Download(source, manga, chapter)
|
||||
|
||||
if (!prepareDownload(download)) {
|
||||
queue.add(download)
|
||||
pending.add(download)
|
||||
}
|
||||
}
|
||||
if (isRunning) downloadsQueueSubject.onNext(pending)
|
||||
}
|
||||
|
||||
// Public method to check if a chapter is downloaded
|
||||
fun isChapterDownloaded(source: Source, manga: Manga, chapter: Chapter): Boolean {
|
||||
val directory = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||
if (!directory.exists())
|
||||
return false
|
||||
|
||||
val pages = getSavedPageList(source, manga, chapter)
|
||||
return isChapterDownloaded(directory, pages)
|
||||
}
|
||||
|
||||
// Prepare the download. Returns true if the chapter is already downloaded
|
||||
private fun prepareDownload(download: Download): Boolean {
|
||||
// If the chapter is already queued, don't add it again
|
||||
for (queuedDownload in queue) {
|
||||
if (download.chapter.id == queuedDownload.chapter.id)
|
||||
return true
|
||||
}
|
||||
|
||||
// Add the directory to the download object for future access
|
||||
download.directory = getAbsoluteChapterDirectory(download)
|
||||
|
||||
// If the directory doesn't exist, the chapter isn't downloaded.
|
||||
if (!download.directory.exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the page list doesn't exist, the chapter isn't downloaded
|
||||
val savedPages = getSavedPageList(download) ?: return false
|
||||
|
||||
// Add the page list to the download object for future access
|
||||
download.pages = savedPages
|
||||
|
||||
// If the number of files matches the number of pages, the chapter is downloaded.
|
||||
// We have the index file, so we check one file more
|
||||
return isChapterDownloaded(download.directory, download.pages)
|
||||
}
|
||||
|
||||
// Check that all the images are downloaded
|
||||
private fun isChapterDownloaded(directory: File, pages: List<Page>?): Boolean {
|
||||
return pages != null && !pages.isEmpty() && pages.size + 1 == directory.listFiles().size
|
||||
}
|
||||
|
||||
// Download the entire chapter
|
||||
private fun downloadChapter(download: Download): Observable<Download> {
|
||||
DiskUtils.createDirectory(download.directory)
|
||||
|
||||
val pageListObservable = if (download.pages == null)
|
||||
// Pull page list from network and add them to download object
|
||||
download.source.pullPageListFromNetwork(download.chapter.url)
|
||||
.doOnNext { pages ->
|
||||
download.pages = pages
|
||||
savePageList(download)
|
||||
}
|
||||
else
|
||||
// Or if the page list already exists, start from the file
|
||||
Observable.just(download.pages)
|
||||
|
||||
return Observable.defer { pageListObservable
|
||||
.doOnNext { pages ->
|
||||
download.downloadedImages = 0
|
||||
download.status = Download.DOWNLOADING
|
||||
}
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.getAllImageUrlsFromPageList(it) }
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
.concatMap { page -> getOrDownloadImage(page, download) }
|
||||
// Do after download completes
|
||||
.doOnCompleted { onDownloadCompleted(download) }
|
||||
.toList()
|
||||
.map { pages -> download }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorResumeNext { error ->
|
||||
download.status = Download.ERROR
|
||||
Observable.just(download)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
// Get the image from the filesystem if it exists or download from network
|
||||
private fun getOrDownloadImage(page: Page, download: Download): Observable<Page> {
|
||||
// If the image URL is empty, do nothing
|
||||
if (page.imageUrl == null)
|
||||
return Observable.just(page)
|
||||
|
||||
val filename = getImageFilename(page)
|
||||
val imagePath = File(download.directory, filename)
|
||||
|
||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||
val pageObservable = if (isImageDownloaded(imagePath))
|
||||
Observable.just(page)
|
||||
else
|
||||
downloadImage(page, download.source, download.directory, filename)
|
||||
|
||||
return pageObservable
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
.doOnNext {
|
||||
page.imagePath = imagePath.absolutePath
|
||||
page.progress = 100
|
||||
download.downloadedImages++
|
||||
page.status = Page.READY
|
||||
}
|
||||
// Mark this page as error and allow to download the remaining
|
||||
.onErrorResumeNext {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
Observable.just(page)
|
||||
}
|
||||
}
|
||||
|
||||
// Save image on disk
|
||||
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
return source.getImageProgressResponse(page)
|
||||
.flatMap {
|
||||
try {
|
||||
val file = File(directory, filename)
|
||||
file.parentFile.mkdirs()
|
||||
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
|
||||
} catch (e: Exception) {
|
||||
it.body().close()
|
||||
throw e
|
||||
}
|
||||
Observable.just(page)
|
||||
}
|
||||
.retryWhen {
|
||||
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
|
||||
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
||||
fun getDownloadedImage(page: Page, chapterDir: File): Observable<Page> {
|
||||
if (page.imageUrl == null) {
|
||||
page.status = Page.ERROR
|
||||
return Observable.just(page)
|
||||
}
|
||||
|
||||
val imagePath = File(chapterDir, getImageFilename(page))
|
||||
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
if (isImageDownloaded(imagePath)) {
|
||||
page.imagePath = imagePath.absolutePath
|
||||
page.progress = 100
|
||||
page.status = Page.READY
|
||||
} else {
|
||||
page.status = Page.ERROR
|
||||
}
|
||||
return Observable.just(page)
|
||||
}
|
||||
|
||||
// Get the filename for an image given the page
|
||||
private fun getImageFilename(page: Page): String {
|
||||
val url = page.imageUrl
|
||||
val number = String.format("%03d", page.pageNumber + 1)
|
||||
|
||||
// Try to preserve file extension
|
||||
return when {
|
||||
UrlUtil.isJpg(url) -> "$number.jpg"
|
||||
UrlUtil.isPng(url) -> "$number.png"
|
||||
UrlUtil.isGif(url) -> "$number.gif"
|
||||
else -> Uri.parse(url).lastPathSegment.replace(imageFilenameRegex, "_")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isImageDownloaded(imagePath: File): Boolean {
|
||||
return imagePath.exists()
|
||||
}
|
||||
|
||||
// Called when a download finishes. This doesn't mean the download was successful, so we check it
|
||||
private fun onDownloadCompleted(download: Download) {
|
||||
checkDownloadIsSuccessful(download)
|
||||
savePageList(download)
|
||||
}
|
||||
|
||||
private fun checkDownloadIsSuccessful(download: Download) {
|
||||
var actualProgress = 0
|
||||
var status = Download.DOWNLOADED
|
||||
// If any page has an error, the download result will be error
|
||||
for (page in download.pages) {
|
||||
actualProgress += page.progress
|
||||
if (page.status != Page.READY) status = Download.ERROR
|
||||
}
|
||||
// Ensure that the chapter folder has all the images
|
||||
if (!isChapterDownloaded(download.directory, download.pages)) {
|
||||
status = Download.ERROR
|
||||
}
|
||||
download.totalProgress = actualProgress
|
||||
download.status = status
|
||||
// Delete successful downloads from queue after notifying
|
||||
if (status == Download.DOWNLOADED) {
|
||||
queue.del(download)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the page list from the chapter's directory if it exists, null otherwise
|
||||
fun getSavedPageList(source: Source, manga: Manga, chapter: Chapter): List<Page>? {
|
||||
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
||||
|
||||
return try {
|
||||
JsonReader(FileReader(pagesFile)).use {
|
||||
val collectionType = object : TypeToken<List<Page>>() {}.type
|
||||
gson.fromJson(it, collectionType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut for the method above
|
||||
private fun getSavedPageList(download: Download): List<Page>? {
|
||||
return getSavedPageList(download.source, download.manga, download.chapter)
|
||||
}
|
||||
|
||||
// Save the page list to the chapter's directory
|
||||
fun savePageList(source: Source, manga: Manga, chapter: Chapter, pages: List<Page>) {
|
||||
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
||||
|
||||
pagesFile.outputStream().use {
|
||||
try {
|
||||
it.write(gson.toJson(pages).toByteArray())
|
||||
it.flush()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut for the method above
|
||||
private fun savePageList(download: Download) {
|
||||
savePageList(download.source, download.manga, download.chapter, download.pages)
|
||||
}
|
||||
|
||||
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
||||
val mangaRelativePath = source.visibleName +
|
||||
File.separator +
|
||||
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||
|
||||
return File(preferences.downloadsDirectory().getOrDefault(), mangaRelativePath)
|
||||
}
|
||||
|
||||
// Get the absolute path to the chapter directory
|
||||
fun getAbsoluteChapterDirectory(source: Source, manga: Manga, chapter: Chapter): File {
|
||||
val chapterRelativePath = chapter.name.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||
|
||||
return File(getAbsoluteMangaDirectory(source, manga), chapterRelativePath)
|
||||
}
|
||||
|
||||
// Shortcut for the method above
|
||||
private fun getAbsoluteChapterDirectory(download: Download): File {
|
||||
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter)
|
||||
}
|
||||
|
||||
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
|
||||
val path = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||
DiskUtils.deleteFiles(path)
|
||||
}
|
||||
|
||||
fun areAllDownloadsFinished(): Boolean {
|
||||
for (download in queue) {
|
||||
if (download.status <= Download.DOWNLOADING)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
/**
|
||||
* Subject for subscribing to downloader status.
|
||||
*/
|
||||
val runningRelay: BehaviorRelay<Boolean>
|
||||
get() = downloader.runningRelay
|
||||
|
||||
/**
|
||||
* Tells the downloader to begin downloads.
|
||||
*
|
||||
* @return true if it's started, false otherwise (empty queue).
|
||||
*/
|
||||
fun startDownloads(): Boolean {
|
||||
if (queue.isEmpty())
|
||||
return false
|
||||
|
||||
if (downloadsSubscription == null || downloadsSubscription!!.isUnsubscribed)
|
||||
initializeSubscriptions()
|
||||
|
||||
val pending = ArrayList<Download>()
|
||||
for (download in queue) {
|
||||
if (download.status != Download.DOWNLOADED) {
|
||||
if (download.status != Download.QUEUE) download.status = Download.QUEUE
|
||||
pending.add(download)
|
||||
}
|
||||
}
|
||||
downloadsQueueSubject.onNext(pending)
|
||||
|
||||
return !pending.isEmpty()
|
||||
return downloader.start()
|
||||
}
|
||||
|
||||
fun stopDownloads() {
|
||||
destroySubscriptions()
|
||||
for (download in queue) {
|
||||
if (download.status == Download.DOWNLOADING) {
|
||||
download.status = Download.ERROR
|
||||
/**
|
||||
* Tells the downloader to stop downloads.
|
||||
*
|
||||
* @param reason an optional reason for being stopped, used to notify the user.
|
||||
*/
|
||||
fun stopDownloads(reason: String? = null) {
|
||||
downloader.stop(reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the downloader to pause downloads.
|
||||
*/
|
||||
fun pauseDownloads() {
|
||||
downloader.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the download queue.
|
||||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun clearQueue(isNotification: Boolean = false) {
|
||||
downloader.clearQueue(isNotification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the downloader to enqueue the given list of chapters.
|
||||
*
|
||||
* @param manga the manga of the chapters.
|
||||
* @param chapters the list of chapters to enqueue.
|
||||
*/
|
||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
downloader.queueChapters(manga, chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the page list of a downloaded chapter.
|
||||
*
|
||||
* @param source the source of the chapter.
|
||||
* @param manga the manga of the chapter.
|
||||
* @param chapter the downloaded chapter.
|
||||
* @return an observable containing the list of pages from the chapter.
|
||||
*/
|
||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
|
||||
return buildPageList(provider.findChapterDir(source, manga, chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the page list of a downloaded chapter.
|
||||
*
|
||||
* @param chapterDir the file where the chapter is downloaded.
|
||||
* @return an observable containing the list of pages from the chapter.
|
||||
*/
|
||||
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
val files = chapterDir?.listFiles().orEmpty()
|
||||
.filter { "image" in it.type.orEmpty() }
|
||||
|
||||
if (files.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
|
||||
files.sortedBy { it.name }
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.READY }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory name for a manga.
|
||||
*
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun getMangaDirName(manga: Manga): String {
|
||||
return provider.getMangaDirName(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory name for the given chapter.
|
||||
*
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun getChapterDirName(chapter: Chapter): String {
|
||||
return provider.getChapterDirName(chapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a source if it exists.
|
||||
*
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return provider.findSourceDir(source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory for the given manga, if it exists.
|
||||
*
|
||||
* @param source the source of the manga.
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
||||
return provider.findMangaDir(source, manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory for the given chapter, if it exists.
|
||||
*
|
||||
* @param source the source of the chapter.
|
||||
* @param manga the manga of the chapter.
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? {
|
||||
return provider.findChapterDir(source, manga, chapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directory of a downloaded chapter.
|
||||
*
|
||||
* @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) {
|
||||
provider.findChapterDir(source, manga, chapter)?.delete()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,271 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
internal class DownloadNotifier(private val context: Context) {
|
||||
/**
|
||||
* Notification builder.
|
||||
*/
|
||||
private val notification by lazy {
|
||||
NotificationCompat.Builder(context)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of download. Used for correct notification icon.
|
||||
*/
|
||||
private var isDownloading = false
|
||||
|
||||
/**
|
||||
* The size of queue on start download.
|
||||
*/
|
||||
var initialQueueSize = 0
|
||||
get() = field
|
||||
set(value) {
|
||||
if (value != 0){
|
||||
isSingleChapter = (value == 1)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Simultaneous download setting > 1.
|
||||
*/
|
||||
var multipleDownloadThreads = false
|
||||
|
||||
/**
|
||||
* Updated when error is thrown
|
||||
*/
|
||||
var errorThrown = false
|
||||
|
||||
/**
|
||||
* Updated when only single page is downloaded
|
||||
*/
|
||||
var isSingleChapter = false
|
||||
|
||||
/**
|
||||
* Updated when paused
|
||||
*/
|
||||
var paused = false
|
||||
|
||||
/**
|
||||
* Shows a notification from this builder.
|
||||
*
|
||||
* @param id the id of the notification.
|
||||
*/
|
||||
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old actions if they exist.
|
||||
*/
|
||||
private fun clearActions() = with(notification) {
|
||||
if (!mActions.isEmpty())
|
||||
mActions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||
* those can only be dismissed by the user.
|
||||
*/
|
||||
fun dismiss() {
|
||||
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 queue the queue containing downloads.
|
||||
*/
|
||||
fun onProgressChange(download: Download, queue: DownloadQueue) {
|
||||
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
|
||||
with(notification) {
|
||||
// Check if first call.
|
||||
if (!isDownloading) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setAutoCancel(false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
isDownloading = true
|
||||
}
|
||||
|
||||
if (multipleDownloadThreads) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
|
||||
// Reset the queue size if the download progress is negative
|
||||
if ((initialQueueSize - queue.size) < 0)
|
||||
initialQueueSize = queue.size
|
||||
|
||||
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
|
||||
notification.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification when download is paused.
|
||||
*/
|
||||
fun onDownloadPaused() {
|
||||
with(notification) {
|
||||
setContentTitle(context.getString(R.string.chapter_paused))
|
||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
|
||||
setAutoCancel(false)
|
||||
setProgress(0, 0, false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
// Resume action
|
||||
addAction(R.drawable.ic_av_play_arrow_grey_img,
|
||||
context.getString(R.string.action_resume),
|
||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context))
|
||||
//Clear action
|
||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||
context.getString(R.string.action_clear),
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context))
|
||||
}
|
||||
|
||||
// Show notification.
|
||||
notification.show()
|
||||
|
||||
// Reset initial values
|
||||
isDownloading = false
|
||||
initialQueueSize = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapter is downloaded.
|
||||
*
|
||||
* @param download download object containing download information.
|
||||
*/
|
||||
fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
|
||||
// Check if last download
|
||||
if (!queue.isEmpty()) {
|
||||
return
|
||||
}
|
||||
// Create notification.
|
||||
with(notification) {
|
||||
val title = download.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.update_check_notification_download_complete))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
|
||||
// Show notification.
|
||||
notification.show()
|
||||
|
||||
// Reset initial values
|
||||
isDownloading = false
|
||||
initialQueueSize = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the downloader receives a warning.
|
||||
*
|
||||
* @param reason the text to show.
|
||||
*/
|
||||
fun onWarning(reason: String) {
|
||||
with(notification) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(reason)
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
notification.show()
|
||||
|
||||
// Reset download information
|
||||
isDownloading = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the downloader receives an error. It's shown as a separate notification to avoid
|
||||
* being overwritten.
|
||||
*
|
||||
* @param error string containing error information.
|
||||
* @param chapter string containing chapter title.
|
||||
*/
|
||||
fun onError(error: String? = null, chapter: String? = null) {
|
||||
// Create notification
|
||||
with(notification) {
|
||||
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
clearActions()
|
||||
setAutoCancel(false)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
isDownloading = false
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
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.Source
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This class is used to provide the directories where the downloads should be saved.
|
||||
* It uses the following path scheme: /<root downloads dir>/<source name>/<manga>/<chapter>
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadProvider(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The root directory for downloads.
|
||||
*/
|
||||
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
||||
UniFile.fromUri(context, Uri.parse(it))
|
||||
}
|
||||
|
||||
init {
|
||||
preferences.downloadsDirectory().asObservable()
|
||||
.skip(1)
|
||||
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a manga. For internal use only.
|
||||
*
|
||||
* @param source the source of the manga.
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
internal fun getMangaDir(source: Source, manga: Manga): UniFile {
|
||||
return downloadsDir
|
||||
.createDirectory(getSourceDirName(source))
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a source if it exists.
|
||||
*
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return downloadsDir.findFile(getSourceDirName(source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a manga if it exists.
|
||||
*
|
||||
* @param source the source of the manga.
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
||||
val sourceDir = findSourceDir(source)
|
||||
return sourceDir?.findFile(getMangaDirName(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? {
|
||||
val mangaDir = findMangaDir(source, manga)
|
||||
return mangaDir?.findFile(getChapterDirName(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory name for a source.
|
||||
*
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun getSourceDirName(source: Source): String {
|
||||
return source.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory name for a manga.
|
||||
*
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun getMangaDirName(manga: Manga): String {
|
||||
return DiskUtil.buildValidFilename(manga.title)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chapter directory name for a chapter.
|
||||
*
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun getChapterDirName(chapter: Chapter): String {
|
||||
return DiskUtil.buildValidFilename(chapter.name)
|
||||
}
|
||||
|
||||
}
|
@ -3,132 +3,177 @@ package eu.kanade.tachiyomi.data.download
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.NetworkInfo.State.CONNECTED
|
||||
import android.net.NetworkInfo.State.DISCONNECTED
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
|
||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||
import eu.kanade.tachiyomi.App
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.powerManager
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This service is used to manage the downloader. The system can decide to stop the service, in
|
||||
* which case the downloader is also stopped. It's also stopped while there's no network available.
|
||||
* While the downloader is running, a wake lock will be held.
|
||||
*/
|
||||
class DownloadService : Service() {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Relay used to know when the service is running.
|
||||
*/
|
||||
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||
|
||||
/**
|
||||
* Starts this service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun start(context: Context) {
|
||||
context.startService(Intent(context, DownloadService::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, DownloadService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var downloadManager: DownloadManager
|
||||
@Inject lateinit var preferences: PreferencesHelper
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var networkChangeSubscription: Subscription? = null
|
||||
private var queueRunningSubscription: Subscription? = null
|
||||
private var isRunning: Boolean = false
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Wake lock to prevent the device to enter sleep mode.
|
||||
*/
|
||||
private val wakeLock by lazy {
|
||||
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscriptions to store while the service is running.
|
||||
*/
|
||||
private lateinit var subscriptions: CompositeSubscription
|
||||
|
||||
/**
|
||||
* Called when the service is created.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
App.get(this).component.inject(this)
|
||||
|
||||
createWakeLock()
|
||||
|
||||
listenQueueRunningChanges()
|
||||
runningRelay.call(true)
|
||||
subscriptions = CompositeSubscription()
|
||||
listenDownloaderState()
|
||||
listenNetworkChanges()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the service is destroyed.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
queueRunningSubscription?.unsubscribe()
|
||||
networkChangeSubscription?.unsubscribe()
|
||||
downloadManager.destroySubscriptions()
|
||||
destroyWakeLock()
|
||||
runningRelay.call(false)
|
||||
subscriptions.unsubscribe()
|
||||
downloadManager.stopDownloads()
|
||||
wakeLock.releaseIfNeeded()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used.
|
||||
*/
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to network changes.
|
||||
*
|
||||
* @see onNetworkStateChanged
|
||||
*/
|
||||
private fun listenNetworkChanges() {
|
||||
networkChangeSubscription = ReactiveNetwork().enableInternetCheck()
|
||||
.observeConnectivity(applicationContext)
|
||||
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ state ->
|
||||
when (state) {
|
||||
ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET -> {
|
||||
// If there are no remaining downloads, destroy the service
|
||||
if (!isRunning && !downloadManager.startDownloads()) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
ConnectivityStatus.MOBILE_CONNECTED -> {
|
||||
if (!preferences.downloadOnlyOverWifi()) {
|
||||
if (!isRunning && !downloadManager.startDownloads()) {
|
||||
stopSelf()
|
||||
}
|
||||
} else if (isRunning) {
|
||||
downloadManager.stopDownloads()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (isRunning) {
|
||||
downloadManager.stopDownloads()
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribe({ state -> onNetworkStateChanged(state)
|
||||
}, { error ->
|
||||
toast(R.string.download_queue_error)
|
||||
stopSelf()
|
||||
})
|
||||
}
|
||||
|
||||
private fun listenQueueRunningChanges() {
|
||||
queueRunningSubscription = downloadManager.runningSubject.subscribe { running ->
|
||||
isRunning = running
|
||||
/**
|
||||
* Called when the network state changes.
|
||||
*
|
||||
* @param connectivity the new network state.
|
||||
*/
|
||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||
when (connectivity.state) {
|
||||
CONNECTED -> {
|
||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
if (!started) stopSelf()
|
||||
}
|
||||
}
|
||||
DISCONNECTED -> {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
||||
}
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
||||
*/
|
||||
private fun listenDownloaderState() {
|
||||
subscriptions += downloadManager.runningRelay.subscribe { running ->
|
||||
if (running)
|
||||
acquireWakeLock()
|
||||
wakeLock.acquireIfNeeded()
|
||||
else
|
||||
releaseWakeLock()
|
||||
wakeLock.releaseIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWakeLock() {
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
|
||||
/**
|
||||
* Releases the wake lock if it's held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
if (isHeld) release()
|
||||
}
|
||||
|
||||
private fun destroyWakeLock() {
|
||||
if (wakeLock != null && wakeLock!!.isHeld) {
|
||||
wakeLock!!.release()
|
||||
wakeLock = null
|
||||
}
|
||||
}
|
||||
|
||||
fun acquireWakeLock() {
|
||||
if (wakeLock != null && !wakeLock!!.isHeld) {
|
||||
wakeLock!!.acquire()
|
||||
}
|
||||
}
|
||||
|
||||
fun releaseWakeLock() {
|
||||
if (wakeLock != null && wakeLock!!.isHeld) {
|
||||
wakeLock!!.release()
|
||||
}
|
||||
/**
|
||||
* Acquires the wake lock if it's not held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
if (!isHeld) acquire()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,135 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This class is used to persist active downloads across application restarts.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadStore(context: Context) {
|
||||
|
||||
/**
|
||||
* Preference file where active downloads are stored.
|
||||
*/
|
||||
private val preferences = context.getSharedPreferences("active_downloads", Context.MODE_PRIVATE)
|
||||
|
||||
/**
|
||||
* Gson instance to serialize/deserialize downloads.
|
||||
*/
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Database helper.
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Counter used to keep the queue order.
|
||||
*/
|
||||
private var counter = 0
|
||||
|
||||
/**
|
||||
* Adds a list of downloads to the store.
|
||||
*
|
||||
* @param downloads the list of downloads to add.
|
||||
*/
|
||||
fun addAll(downloads: List<Download>) {
|
||||
val editor = preferences.edit()
|
||||
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a download from the store.
|
||||
*
|
||||
* @param download the download to remove.
|
||||
*/
|
||||
fun remove(download: Download) {
|
||||
preferences.edit().remove(getKey(download)).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the downloads from the store.
|
||||
*/
|
||||
fun clear() {
|
||||
preferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the preference's key for the given download.
|
||||
*
|
||||
* @param download the download.
|
||||
*/
|
||||
private fun getKey(download: Download): String {
|
||||
return download.chapter.id!!.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of downloads to restore. It should be called in a background thread.
|
||||
*/
|
||||
fun restore(): List<Download> {
|
||||
val objs = preferences.all
|
||||
.mapNotNull { it.value as? String }
|
||||
.map { deserialize(it) }
|
||||
.sortedBy { it.order }
|
||||
|
||||
val downloads = mutableListOf<Download>()
|
||||
if (objs.isNotEmpty()) {
|
||||
val cachedManga = mutableMapOf<Long, Manga?>()
|
||||
for ((mangaId, chapterId) in objs) {
|
||||
val manga = cachedManga.getOrPut(mangaId) {
|
||||
db.getManga(mangaId).executeAsBlocking()
|
||||
} ?: continue
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
|
||||
val chapter = db.getChapter(chapterId).executeAsBlocking() ?: continue
|
||||
downloads.add(Download(source, manga, chapter))
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the store, downloads will be added again immediately.
|
||||
clear()
|
||||
return downloads
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a download to a string.
|
||||
*
|
||||
* @param download the download to serialize.
|
||||
*/
|
||||
private fun serialize(download: Download): String {
|
||||
val obj = DownloadObject(download.manga.id!!, download.chapter.id!!, counter++)
|
||||
return gson.toJson(obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a download from a string.
|
||||
*
|
||||
* @param string the download as string.
|
||||
*/
|
||||
private fun deserialize(string: String): DownloadObject {
|
||||
return gson.fromJson(string, DownloadObject::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used for download serialization
|
||||
*
|
||||
* @param mangaId the id of the manga.
|
||||
* @param chapterId the id of the chapter.
|
||||
* @param order the order of the download in the queue.
|
||||
*/
|
||||
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
||||
|
||||
}
|
@ -0,0 +1,466 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.BehaviorSubject
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
*
|
||||
* Its [queue] contains the list of chapters to download. In order to download them, the downloader
|
||||
* subscriptions must be running and the list of chapters must be sent to them by [downloadsRelay].
|
||||
*
|
||||
* The queue manipulation must be done in one thread (currently the main thread) to avoid unexpected
|
||||
* behavior, but it's safe to read it from multiple threads.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @param provider the downloads directory provider.
|
||||
*/
|
||||
class Downloader(private val context: Context, private val provider: DownloadProvider) {
|
||||
|
||||
/**
|
||||
* Store for persisting downloads across restarts.
|
||||
*/
|
||||
private val store = DownloadStore(context)
|
||||
|
||||
/**
|
||||
* Queue where active downloads are kept.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
private val notifier by lazy { DownloadNotifier(context) }
|
||||
|
||||
/**
|
||||
* Downloader subscriptions.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<List<Download>>()
|
||||
|
||||
/**
|
||||
* Relay to subscribe to the downloader status.
|
||||
*/
|
||||
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||
|
||||
/**
|
||||
* Whether the downloader is running.
|
||||
*/
|
||||
@Volatile private var isRunning: Boolean = false
|
||||
|
||||
init {
|
||||
Observable.fromCallable { store.restore() }
|
||||
.map { downloads -> downloads.filter { isDownloadAllowed(it) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ downloads -> queue.addAll(downloads)
|
||||
}, { error -> Timber.e(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the downloader. It doesn't do anything if it's already running or there isn't anything
|
||||
* to download.
|
||||
*
|
||||
* @return true if the downloader is started, false otherwise.
|
||||
*/
|
||||
fun start(): Boolean {
|
||||
if (isRunning || queue.isEmpty())
|
||||
return false
|
||||
|
||||
if (!subscriptions.hasSubscriptions())
|
||||
initializeSubscriptions()
|
||||
|
||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||
|
||||
// Show download notification when simultaneous download > 1.
|
||||
notifier.onProgressChange(queue)
|
||||
|
||||
downloadsRelay.call(pending)
|
||||
return !pending.isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the downloader.
|
||||
*/
|
||||
fun stop(reason: String? = null) {
|
||||
destroySubscriptions()
|
||||
queue
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.ERROR }
|
||||
|
||||
if (reason != null) {
|
||||
notifier.onWarning(reason)
|
||||
} else {
|
||||
if (notifier.paused) {
|
||||
notifier.paused = false
|
||||
notifier.onDownloadPaused()
|
||||
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||
notifier.isSingleChapter = false
|
||||
} else {
|
||||
notifier.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the downloader
|
||||
*/
|
||||
fun pause() {
|
||||
destroySubscriptions()
|
||||
queue
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.QUEUE }
|
||||
notifier.paused = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes everything from the queue.
|
||||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun clearQueue(isNotification: Boolean = false) {
|
||||
destroySubscriptions()
|
||||
|
||||
//Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue
|
||||
.filter { it.status == Download.QUEUE }
|
||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
notifier.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the subscriptions to start downloading.
|
||||
*/
|
||||
private fun initializeSubscriptions() {
|
||||
if (isRunning) return
|
||||
isRunning = true
|
||||
runningRelay.call(true)
|
||||
|
||||
subscriptions.clear()
|
||||
|
||||
subscriptions += preferences.downloadThreads().asObservable()
|
||||
.subscribe {
|
||||
threadsSubject.onNext(it)
|
||||
notifier.multipleDownloadThreads = it > 1
|
||||
}
|
||||
|
||||
subscriptions += downloadsRelay.flatMap { Observable.from(it) }
|
||||
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ completeDownload(it)
|
||||
}, { error ->
|
||||
DownloadService.stop(context)
|
||||
Timber.e(error)
|
||||
notifier.onError(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the downloader subscriptions.
|
||||
*/
|
||||
private fun destroySubscriptions() {
|
||||
if (!isRunning) return
|
||||
isRunning = false
|
||||
runningRelay.call(false)
|
||||
|
||||
subscriptions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a download object for every chapter and adds them to the downloads queue. This method
|
||||
* must be called in the main thread.
|
||||
*
|
||||
* @param manga the manga of the chapters to download.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return
|
||||
|
||||
val chaptersToQueue = chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
// Create a downloader for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given download can be queued and downloaded.
|
||||
*
|
||||
* @param download the download to be checked.
|
||||
*/
|
||||
private fun isDownloadAllowed(download: Download): Boolean {
|
||||
// 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 (dir != null && dir.exists())
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable which downloads a chapter.
|
||||
*
|
||||
* @param download the chapter to be downloaded.
|
||||
*/
|
||||
private fun downloadChapter(download: Download): Observable<Download> {
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
val mangaDir = provider.getMangaDir(download.source, download.manga)
|
||||
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
|
||||
|
||||
val pageListObservable = if (download.pages == null) {
|
||||
// Pull page list from network and add them to download object
|
||||
download.source.fetchPageList(download.chapter)
|
||||
.doOnNext { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
download.pages = pages
|
||||
}
|
||||
} else {
|
||||
// Or if the page list already exists, start from the file
|
||||
Observable.just(download.pages!!)
|
||||
}
|
||||
|
||||
return pageListObservable
|
||||
.doOnNext { pages ->
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.name!!.endsWith(".tmp") }
|
||||
?.forEach { it.delete() }
|
||||
|
||||
download.downloadedImages = 0
|
||||
download.status = Download.DOWNLOADING
|
||||
}
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download, queue) }
|
||||
.toList()
|
||||
.map { pages -> download }
|
||||
// Do after download completes
|
||||
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorReturn { error ->
|
||||
download.status = Download.ERROR
|
||||
notifier.onError(error.message, download.chapter.name)
|
||||
download
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable which gets the image from the filesystem if it exists or downloads it
|
||||
* otherwise.
|
||||
*
|
||||
* @param page the page to download.
|
||||
* @param download the download of the page.
|
||||
* @param tmpDir the temporary directory of the download.
|
||||
*/
|
||||
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
|
||||
// If the image URL is empty, do nothing
|
||||
if (page.imageUrl == null)
|
||||
return Observable.just(page)
|
||||
|
||||
val filename = String.format("%03d", page.number)
|
||||
val tmpFile = tmpDir.findFile("$filename.tmp")
|
||||
|
||||
// Delete temp file if it exists.
|
||||
tmpFile?.delete()
|
||||
|
||||
// Try to find the image file.
|
||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
||||
|
||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||
val pageObservable = if (imageFile != null)
|
||||
Observable.just(imageFile)
|
||||
else
|
||||
downloadImage(page, download.source, tmpDir, filename)
|
||||
|
||||
return pageObservable
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
.doOnNext { file ->
|
||||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
download.downloadedImages++
|
||||
page.status = Page.READY
|
||||
}
|
||||
.map { page }
|
||||
// Mark this page as error and allow to download the remaining
|
||||
.onErrorReturn {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
page
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable which downloads the image from network.
|
||||
*
|
||||
* @param page the page to download.
|
||||
* @param source the source of the page.
|
||||
* @param tmpDir the temporary directory of the download.
|
||||
* @param filename the filename of the image.
|
||||
*/
|
||||
private fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
page.progress = 0
|
||||
return source.fetchImage(page)
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
response.body()!!.source().saveTo(file.openOutputStream())
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
response.close()
|
||||
file.delete()
|
||||
throw e
|
||||
}
|
||||
file
|
||||
}
|
||||
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension of the downloaded image from the network response, or if it's null,
|
||||
* analyze the file. If everything fails, assume it's a jpg.
|
||||
*
|
||||
* @param response the network response of the image.
|
||||
* @param file the file where the image is already downloaded.
|
||||
*/
|
||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||
// Read content type if available.
|
||||
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||
// Else guess from the uri.
|
||||
?: context.contentResolver.getType(file.uri)
|
||||
// Else read magic numbers.
|
||||
?: DiskUtil.findImageMime { file.openInputStream() }
|
||||
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the download was successful.
|
||||
*
|
||||
* @param download the download to check.
|
||||
* @param tmpDir the directory where the download is currently stored.
|
||||
* @param dirname the real (non temporary) directory name of the download.
|
||||
*/
|
||||
private fun ensureSuccessfulDownload(download: Download, tmpDir: UniFile, dirname: String) {
|
||||
// Ensure that the chapter folder has all the images.
|
||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||
|
||||
download.status = if (downloadedImages.size == download.pages!!.size) {
|
||||
Download.DOWNLOADED
|
||||
} else {
|
||||
Download.ERROR
|
||||
}
|
||||
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (download.status == Download.DOWNLOADED) {
|
||||
tmpDir.renameTo(dirname)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a download. This method is called in the main thread.
|
||||
*/
|
||||
private fun completeDownload(download: Download) {
|
||||
// Delete successful downloads from queue
|
||||
if (download.status == Download.DOWNLOADED) {
|
||||
// remove downloaded chapter from queue
|
||||
queue.remove(download)
|
||||
notifier.onProgressChange(queue)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||
notifier.onDownloadCompleted(download, queue)
|
||||
}
|
||||
DownloadService.stop(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
|
||||
*/
|
||||
private fun areAllDownloadsFinished(): Boolean {
|
||||
return queue.none { it.status <= Download.DOWNLOADING }
|
||||
}
|
||||
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.download.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
public class Download {
|
||||
public Source source;
|
||||
public Manga manga;
|
||||
public Chapter chapter;
|
||||
public List<Page> pages;
|
||||
public File directory;
|
||||
|
||||
public transient volatile int totalProgress;
|
||||
public transient volatile int downloadedImages;
|
||||
private transient volatile int status;
|
||||
|
||||
private transient PublishSubject<Download> statusSubject;
|
||||
|
||||
public static final int NOT_DOWNLOADED = 0;
|
||||
public static final int QUEUE = 1;
|
||||
public static final int DOWNLOADING = 2;
|
||||
public static final int DOWNLOADED = 3;
|
||||
public static final int ERROR = 4;
|
||||
|
||||
|
||||
public Download(Source source, Manga manga, Chapter chapter) {
|
||||
this.source = source;
|
||||
this.manga = manga;
|
||||
this.chapter = chapter;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(int status) {
|
||||
this.status = status;
|
||||
notifyStatus();
|
||||
}
|
||||
|
||||
public void setStatusSubject(PublishSubject<Download> subject) {
|
||||
this.statusSubject = subject;
|
||||
}
|
||||
|
||||
private void notifyStatus() {
|
||||
if (statusSubject != null)
|
||||
statusSubject.onNext(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package eu.kanade.tachiyomi.data.download.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||
|
||||
var pages: List<Page>? = null
|
||||
|
||||
@Volatile @Transient var totalProgress: Int = 0
|
||||
|
||||
@Volatile @Transient var downloadedImages: Int = 0
|
||||
|
||||
@Volatile @Transient var status: Int = 0
|
||||
set(status) {
|
||||
field = status
|
||||
statusSubject?.onNext(this)
|
||||
}
|
||||
|
||||
@Transient private var statusSubject: PublishSubject<Download>? = null
|
||||
|
||||
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||
statusSubject = subject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val NOT_DOWNLOADED = 0
|
||||
const val QUEUE = 1
|
||||
const val DOWNLOADING = 2
|
||||
const val DOWNLOADED = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
}
|
@ -1,39 +1,62 @@
|
||||
package eu.kanade.tachiyomi.data.download.model
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class DownloadQueue : CopyOnWriteArrayList<Download>() {
|
||||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
|
||||
: List<Download> by queue {
|
||||
|
||||
private val statusSubject = PublishSubject.create<Download>()
|
||||
|
||||
override fun add(download: Download): Boolean {
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.status = Download.QUEUE
|
||||
return super.add(download)
|
||||
private val updatedRelay = PublishRelay.create<Unit>()
|
||||
|
||||
fun addAll(downloads: List<Download>) {
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.status = Download.QUEUE
|
||||
}
|
||||
queue.addAll(downloads)
|
||||
store.addAll(downloads)
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
|
||||
fun del(download: Download) {
|
||||
super.remove(download)
|
||||
fun remove(download: Download) {
|
||||
val removed = queue.remove(download)
|
||||
store.remove(download)
|
||||
download.setStatusSubject(null)
|
||||
}
|
||||
|
||||
fun del(chapter: Chapter) {
|
||||
for (download in this) {
|
||||
if (download.chapter.id == chapter.id) {
|
||||
del(download)
|
||||
break
|
||||
}
|
||||
if (removed) {
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun getActiveDownloads() =
|
||||
fun remove(chapter: Chapter) {
|
||||
find { it.chapter.id == chapter.id }?.let { remove(it) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
queue.forEach { download ->
|
||||
download.setStatusSubject(null)
|
||||
}
|
||||
queue.clear()
|
||||
store.clear()
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
|
||||
fun getActiveDownloads(): Observable<Download> =
|
||||
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
||||
|
||||
fun getStatusObservable() = statusSubject.onBackpressureBuffer()
|
||||
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||
|
||||
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
|
||||
.startWith(Unit)
|
||||
.map { this }
|
||||
|
||||
fun getProgressObservable(): Observable<Download> {
|
||||
return statusSubject.onBackpressureBuffer()
|
||||
|
@ -0,0 +1,32 @@
|
||||
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())
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority): InputStream {
|
||||
data = file.inputStream()
|
||||
return data!!
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
data?.let { data ->
|
||||
try {
|
||||
data.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return file.toString()
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
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.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* 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:
|
||||
*
|
||||
* - Check in RAM LRU.
|
||||
* - Check in disk LRU.
|
||||
* - Check in this module.
|
||||
* - Fetch from the network connection.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
*/
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Base network loader.
|
||||
*/
|
||||
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
||||
InputStream::class.java, context)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100)
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
*/
|
||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
||||
|
||||
/**
|
||||
* Factory class for creating [MangaModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||
|
||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
||||
= MangaModelLoader(context)
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
* @param manga the model.
|
||||
* @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.
|
||||
*/
|
||||
override fun getResourceFetcher(manga: Manga,
|
||||
width: Int,
|
||||
height: Int): DataFetcher<InputStream>? {
|
||||
|
||||
// Check thumbnail is not null or empty
|
||||
val url = manga.thumbnail_url
|
||||
if (url == null || url.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (url.startsWith("http")) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
|
||||
// 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.
|
||||
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
|
||||
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return MangaUrlFetcher(networkFetcher, file, manga)
|
||||
} else {
|
||||
// Get the file from the url, removing the scheme if present.
|
||||
val file = File(url.substringAfter("file://"))
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return MangaFileFetcher(file, manga)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
||||
*
|
||||
* @param manga the model.
|
||||
*/
|
||||
fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||
if (source == null) return LazyHeaders.DEFAULT
|
||||
|
||||
return cachedHeaders.getOrPut(manga.source) {
|
||||
LazyHeaders.Builder().apply {
|
||||
val nullStr: String? = null
|
||||
setHeader("User-Agent", nullStr)
|
||||
for ((key, value) in source.headers.toMultimap()) {
|
||||
addHeader(key, value[0])
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user