mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
257 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
e2ecf0ce5f | |||
5d396bfb7c | |||
de6cc8394e | |||
4b7159648a | |||
eb9c5f95db | |||
55e9d2880c | |||
ec9c19ce7d | |||
31731e8f26 | |||
bfb12bc7c1 | |||
4befcf3819 | |||
cb58145361 | |||
b83efd90a8 | |||
9f0da3f1d6 | |||
50ae08ed8d | |||
5385642a5b | |||
0a27d4e185 | |||
bd8b9febd2 | |||
a30705f197 | |||
877032a757 | |||
19bf47b6d2 | |||
a9bfeb058b | |||
9213fc6999 | |||
447dfd1e3c | |||
638d3a32cf | |||
17c59657c3 | |||
81bce8ef76 | |||
78314077bb | |||
a7840bc247 | |||
6d0254c5e5 | |||
06681a3db7 |
7
.github/CONTRIBUTING.md
vendored
7
.github/CONTRIBUTING.md
vendored
@ -1,19 +1,20 @@
|
|||||||
# Bugs
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (Setting > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* 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 steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
* If it could be device-dependent, try reproducing on another device (if possible), 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).**
|
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* For multipart issues use list like this:
|
* For multipart issues **use list** like this:
|
||||||
* [x] Done
|
* [x] Done
|
||||||
* [ ] Not done
|
* [ ] Not done
|
||||||
```
|
```
|
||||||
* [x] Done
|
* [x] Done
|
||||||
* [ ] Not done
|
* [ ] Not done
|
||||||
```
|
```
|
||||||
|
* Don't put together too many unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
|
11
.travis.yml
11
.travis.yml
@ -5,17 +5,20 @@ android:
|
|||||||
- tools
|
- tools
|
||||||
|
|
||||||
# The BuildTools version used by your project
|
# The BuildTools version used by your project
|
||||||
- build-tools-23.0.3
|
- build-tools-24.0.2
|
||||||
- android-23
|
- android-24
|
||||||
- extra-android-m2repository
|
- extra-android-m2repository
|
||||||
- extra-google-m2repository
|
- extra-google-m2repository
|
||||||
- extra-android-support
|
- extra-android-support
|
||||||
- extra-google-google_play_services
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
jdk:
|
||||||
|
- oraclejdk8
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- chmod +x gradlew
|
- chmod +x gradlew
|
||||||
#Build, and run tests
|
#Build, and run tests
|
||||||
script: "./gradlew clean assembleDebug testDebugUnitTest"
|
script: "./gradlew clean buildDebug"
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
@ -25,4 +28,4 @@ cache:
|
|||||||
- $HOME/.gradle/caches/
|
- $HOME/.gradle/caches/
|
||||||
- $HOME/.gradle/wrapper/
|
- $HOME/.gradle/wrapper/
|
||||||
env:
|
env:
|
||||||
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
| Build | Download | Auto Update |
|
| 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) |
|
| [](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) |
|
||||||
|
|
||||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
custom.gradle
|
132
app/build.gradle
132
app/build.gradle
@ -4,11 +4,15 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
if (file("custom.gradle").exists()) {
|
||||||
|
apply from: "custom.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// Git is needed in your system PATH for these commands to work.
|
// Git is needed in your system PATH for these commands to work.
|
||||||
// If it's not installed, you can return a random value as a workaround
|
// If it's not installed, you can return a random value as a workaround
|
||||||
getCommitCount = {
|
getCommitCount = {
|
||||||
return 'git rev-list --count origin/master'.execute().text.trim()
|
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||||
// return "1"
|
// return "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,17 +33,17 @@ def includeUpdater() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion 24
|
||||||
buildToolsVersion "23.0.3"
|
buildToolsVersion "24.0.2"
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 24
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 6
|
versionCode 11
|
||||||
versionName "0.2.0"
|
versionName "0.3.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -47,16 +51,22 @@ android {
|
|||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi", "armeabi-v7a", "x86"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
versionNameSuffix ".${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
|
multiDexEnabled true
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
|
multiDexEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,110 +88,112 @@ android {
|
|||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
|
|
||||||
useLibrary 'org.apache.http.legacy'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
kapt {
|
|
||||||
generateStubs = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
final SUPPORT_LIBRARY_VERSION = '23.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
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
|
compile 'com.github.inorichi:subsampling-scale-image-view:2d9c854'
|
||||||
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
final support_library_version = '24.2.1'
|
||||||
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:support-v4:$support_library_version"
|
||||||
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||||
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:cardview-v7:$support_library_version"
|
||||||
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:design:$support_library_version"
|
||||||
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:recyclerview-v7:$support_library_version"
|
||||||
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:support-annotations:$support_library_version"
|
||||||
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:customtabs:$support_library_version"
|
||||||
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
|
||||||
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
|
compile 'com.google.android.gms:play-services-gcm:9.6.1'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.1.0'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.1.1'
|
compile 'io.reactivex:rxjava:1.2.1'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
compile "com.squareup.okhttp3:okhttp:3.4.1"
|
||||||
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
|
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
final retrofit_version = '2.1.0'
|
||||||
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
|
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
|
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
|
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// IO
|
// IO
|
||||||
compile 'com.squareup.okio:okio:1.7.0'
|
compile 'com.squareup.okio:okio:1.10.0'
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.6.2'
|
compile 'com.google.code.gson:gson:2.7'
|
||||||
|
compile 'com.github.salomonbrys.kotson:kotson:2.4.0'
|
||||||
|
|
||||||
|
// YAML
|
||||||
|
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
compile 'com.squareup.duktape:duktape-android:1.0.0'
|
||||||
|
|
||||||
// Disk cache
|
// Disk cache
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
|
||||||
// Parse HTML
|
// Parse HTML
|
||||||
compile 'org.jsoup:jsoup:1.8.3'
|
compile 'org.jsoup:jsoup:1.9.2'
|
||||||
|
|
||||||
|
// Changelog
|
||||||
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
compile "com.pushtorefresh.storio:sqlite:1.11.0"
|
||||||
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
|
||||||
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
compile 'info.android15.nucleus:nucleus:2.0.5'
|
final nucleus_version = '3.0.0'
|
||||||
|
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
||||||
|
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
|
||||||
|
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
|
||||||
provided 'org.glassfish:javax.annotation:10.0-b28'
|
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||||
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||||
|
// Transformations
|
||||||
|
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
compile 'com.jakewharton.timber:timber:4.3.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
compile 'ch.acra:acra:4.8.5'
|
compile 'ch.acra:acra:4.9.1'
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
|
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.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:4.2.0'
|
||||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
compile('com.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
|
compile 'com.afollestad.material-dialogs:core:0.9.0.2'
|
||||||
transitive = true
|
compile 'net.xpece.android:support-preference:1.0.3'
|
||||||
}
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
|
compile 'de.hdodenhof:circleimageview:2.1.0'
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:1.7.1'
|
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||||
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
|
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||||
testCompile('org.robolectric:robolectric:3.0') {
|
testCompile 'org.robolectric:robolectric:3.1.2'
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
testCompile 'org.robolectric:shadows-multidex:3.1.2'
|
||||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
testCompile 'org.robolectric:shadows-play-services:3.1.2'
|
||||||
}
|
|
||||||
|
|
||||||
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.1'
|
ext.kotlin_version = '1.0.4'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
12
app/proguard-rules.pro
vendored
12
app/proguard-rules.pro
vendored
@ -1,7 +1,5 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep class eu.kanade.tachiyomi.injection.** { *; }
|
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
@ -118,4 +116,12 @@
|
|||||||
|
|
||||||
# Keep the support library
|
# Keep the support library
|
||||||
-keep class org.acra.** { *; }
|
-keep class org.acra.** { *; }
|
||||||
-keep interface org.acra.** { *; }
|
-keep interface org.acra.** { *; }
|
||||||
|
|
||||||
|
# SnakeYaml
|
||||||
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
|
-dontwarn org.yaml.snakeyaml.**
|
||||||
|
|
||||||
|
# Duktape
|
||||||
|
-keep class com.squareup.duktape.** { *; }
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest package="eu.kanade.tachiyomi"
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@ -8,6 +10,8 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
|
||||||
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -18,8 +22,7 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi" >
|
android:theme="@style/Theme.Tachiyomi" >
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity">
|
||||||
android:theme="@style/Theme.BrandedLaunch">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@ -28,7 +31,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
android:parentActivityName=".ui.main.MainActivity" >
|
android:parentActivityName=".ui.main.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
@ -59,28 +63,35 @@
|
|||||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<receiver
|
<service
|
||||||
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
android:name=".data.library.LibraryUpdateTrigger"
|
||||||
android:enabled="false">
|
android:exported="true"
|
||||||
|
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.updater.UpdateCheckerService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".data.updater.UpdateDownloaderService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.library.LibraryUpdateService$LibraryUpdateReceiver">
|
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
||||||
</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>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
android:value="GlideModule" />
|
android:value="GlideModule" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
@ -2,14 +2,13 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import android.support.multidex.MultiDex
|
||||||
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 org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
|
|
||||||
@ReportsCrashes(
|
@ReportsCrashes(
|
||||||
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
||||||
@ -20,43 +19,25 @@ import timber.log.Timber
|
|||||||
)
|
)
|
||||||
open class App : Application() {
|
open class App : Application() {
|
||||||
|
|
||||||
lateinit var component: AppComponent
|
|
||||||
private set
|
|
||||||
|
|
||||||
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
|
|
||||||
private set
|
|
||||||
|
|
||||||
var appTheme = 0
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
component = prepareAppComponent().build()
|
|
||||||
|
|
||||||
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
|
|
||||||
|
|
||||||
setupTheme()
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTheme() {
|
override fun attachBaseContext(base: Context) {
|
||||||
appTheme = PreferencesHelper.getTheme(this)
|
super.attachBaseContext(base)
|
||||||
}
|
if (BuildConfig.DEBUG) {
|
||||||
|
MultiDex.install(this)
|
||||||
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
|
}
|
||||||
return DaggerAppComponent.builder()
|
|
||||||
.appModule(AppModule(this))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
ACRA.init(this)
|
ACRA.init(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun get(context: Context): App {
|
|
||||||
return context.applicationContext as App
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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.mangasync.MangaSyncManager
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.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 { MangaSyncManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
app/src/main/java/eu/kanade/tachiyomi/Constants.kt
Normal file
8
app/src/main/java/eu/kanade/tachiyomi/Constants.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
object Constants {
|
||||||
|
const val NOTIFICATION_LIBRARY_ID = 1
|
||||||
|
const val NOTIFICATION_UPDATER_ID = 2
|
||||||
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
||||||
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
||||||
|
}
|
@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,8 +190,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
private fun restoreCategories(jsonCategories: JsonArray) {
|
private fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = db.getCategories().executeAsBlocking()
|
||||||
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
|
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
object : TypeToken<List<Category>>() {}.type)
|
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
for (category in backupCategories) {
|
for (category in backupCategories) {
|
||||||
@ -224,17 +222,13 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
||||||
*/
|
*/
|
||||||
private fun restoreMangas(jsonMangas: JsonArray) {
|
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) {
|
for (backupManga in jsonMangas) {
|
||||||
// Map every entry to objects
|
// Map every entry to objects
|
||||||
val element = backupManga.asJsonObject
|
val element = backupManga.asJsonObject
|
||||||
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
|
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
||||||
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
|
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
||||||
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
|
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
|
||||||
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
|
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
||||||
|
|
||||||
// Restore everything related to this manga
|
// Restore everything related to this manga
|
||||||
restoreManga(manga)
|
restoreManga(manga)
|
||||||
@ -340,7 +334,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
for (mangaSync in sync) {
|
for (mangaSync in sync) {
|
||||||
mangaSync.manga_id = manga.id
|
mangaSync.manga_id = manga.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
||||||
@ -367,15 +361,4 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of items from a json element, or an empty list if the element is null.
|
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
|
|
||||||
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import com.google.gson.reflect.TypeToken
|
|||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils
|
import eu.kanade.tachiyomi.util.DiskUtils
|
||||||
|
import eu.kanade.tachiyomi.util.saveImageTo
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Okio
|
import okio.Okio
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -184,7 +185,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
* @throws IOException image error.
|
* @throws IOException image error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun putImageToCache(imageUrl: String, response: Response) {
|
fun putImageToCache(imageUrl: String, response: Response, reencode: Boolean) {
|
||||||
// Initialize editor (edits the values for an entry).
|
// Initialize editor (edits the values for an entry).
|
||||||
var editor: DiskLruCache.Editor? = null
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
@ -194,17 +195,12 @@ class ChapterCache(private val context: Context) {
|
|||||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||||
|
|
||||||
// Get OutputStream and write image with Okio.
|
// Get OutputStream and write image with Okio.
|
||||||
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
|
response.body().source().saveImageTo(editor.newOutputStream(0), reencode)
|
||||||
it.writeAll(response.body().source())
|
|
||||||
it.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
} catch (e: Exception) {
|
|
||||||
response.body().close()
|
|
||||||
throw IOException("Unable to save image")
|
|
||||||
} finally {
|
} finally {
|
||||||
|
response.body().close()
|
||||||
editor?.abortUnlessCommitted()
|
editor?.abortUnlessCommitted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
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.DiskUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -27,80 +22,19 @@ class CoverCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param thumbnailUrl the thumbnail url.
|
||||||
* @return cover image.
|
* @return cover image.
|
||||||
*/
|
*/
|
||||||
private fun getCoverFromCache(thumbnailUrl: String): File {
|
fun getCoverFile(thumbnailUrl: String): File {
|
||||||
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy the given stream to this cache.
|
* Copy the given stream to this cache.
|
||||||
|
*
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param thumbnailUrl url of the thumbnail.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
||||||
// Get destination file.
|
// Get destination file.
|
||||||
val destFile = getCoverFromCache(thumbnailUrl)
|
val destFile = getCoverFile(thumbnailUrl)
|
||||||
|
|
||||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
destFile.outputStream().use { inputStream.copyTo(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the cover file from the cache.
|
* Delete the cover file from the cache.
|
||||||
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param thumbnailUrl the thumbnail url.
|
||||||
* @return status of deletion.
|
* @return status of deletion.
|
||||||
*/
|
*/
|
||||||
@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Remove file.
|
// Remove file.
|
||||||
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
val file = getCoverFile(thumbnailUrl!!)
|
||||||
return file.exists() && file.delete()
|
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,307 +1,27 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
import android.content.Context
|
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.impl.DefaultStorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
|
import eu.kanade.tachiyomi.data.database.mappers.*
|
||||||
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.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.queries.*
|
||||||
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.*
|
|
||||||
|
|
||||||
open class DatabaseHelper(context: Context) {
|
/**
|
||||||
|
* This class provides operations to manage the database through its interfaces.
|
||||||
|
*/
|
||||||
|
open class DatabaseHelper(context: Context)
|
||||||
|
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(DbOpenHelper(context))
|
||||||
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
|
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
|
||||||
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(func: DatabaseHelper.() -> Unit) {
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
db.internal().beginTransaction()
|
|
||||||
try {
|
|
||||||
func()
|
|
||||||
db.internal().setTransactionSuccessful()
|
|
||||||
} finally {
|
|
||||||
db.internal().endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 android.database.sqlite.SQLiteOpenHelper
|
||||||
import eu.kanade.tachiyomi.data.database.tables.*
|
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 {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@ -16,28 +17,41 @@ class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DA
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 1
|
const val DATABASE_VERSION = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
execSQL(MangaTable.getCreateTableQuery())
|
execSQL(MangaTable.createTableQuery)
|
||||||
execSQL(ChapterTable.getCreateTableQuery())
|
execSQL(ChapterTable.createTableQuery)
|
||||||
execSQL(MangaSyncTable.getCreateTableQuery())
|
execSQL(MangaSyncTable.createTableQuery)
|
||||||
execSQL(CategoryTable.getCreateTableQuery())
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.getCreateTableQuery())
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
|
execSQL(HistoryTable.createTableQuery)
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.getCreateUrlIndexQuery())
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
execSQL(MangaTable.getCreateFavoriteIndexQuery())
|
execSQL(MangaTable.createFavoriteIndexQuery)
|
||||||
execSQL(ChapterTable.getCreateMangaIdIndexQuery())
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
db.setForeignKeyConstraintsEnabled(true)
|
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,82 @@
|
|||||||
|
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_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_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(10).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_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))
|
||||||
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 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,62 @@
|
|||||||
|
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.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 = History().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,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.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
|
||||||
|
|
||||||
|
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
|
||||||
|
MangaSyncPutResolver(),
|
||||||
|
MangaSyncGetResolver(),
|
||||||
|
MangaSyncDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: MangaSync) = 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 MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().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 MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: MangaSync) = 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.getInt(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()
|
||||||
|
}
|
@ -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,36 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Chapter : Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var manga_id: Long?
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var read: Boolean
|
||||||
|
|
||||||
|
var last_page_read: Int
|
||||||
|
|
||||||
|
var date_fetch: Long
|
||||||
|
|
||||||
|
var date_upload: Long
|
||||||
|
|
||||||
|
var chapter_number: Float
|
||||||
|
|
||||||
|
var source_order: Int
|
||||||
|
|
||||||
|
val isRecognizedNumber: Boolean
|
||||||
|
get() = chapter_number >= 0f
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(): Chapter = ChapterImpl().apply {
|
||||||
|
chapter_number = -1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
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 read: 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,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing the history statistics of a chapter
|
||||||
|
*/
|
||||||
|
class History : Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of history object.
|
||||||
|
*/
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id of history object.
|
||||||
|
*/
|
||||||
|
var chapter_id: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time chapter was read in time long format
|
||||||
|
*/
|
||||||
|
var last_read: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time chapter was read - todo not yet implemented
|
||||||
|
*/
|
||||||
|
var time_read: Long = 0
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History constructor
|
||||||
|
*
|
||||||
|
* @param chapter chapter object
|
||||||
|
* @return history object
|
||||||
|
*/
|
||||||
|
fun create(chapter: Chapter): History {
|
||||||
|
val history = History()
|
||||||
|
history.chapter_id = chapter.id!!
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,194 +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 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,131 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Manga : Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var source: Int
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
var artist: String?
|
||||||
|
|
||||||
|
var author: String?
|
||||||
|
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
var genre: String?
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var thumbnail_url: String?
|
||||||
|
|
||||||
|
var favorite: Boolean
|
||||||
|
|
||||||
|
var last_update: Long
|
||||||
|
|
||||||
|
var initialized: Boolean
|
||||||
|
|
||||||
|
var viewer: Int
|
||||||
|
|
||||||
|
var chapter_flags: Int
|
||||||
|
|
||||||
|
var unread: Int
|
||||||
|
|
||||||
|
var category: Int
|
||||||
|
|
||||||
|
fun copyFrom(other: Manga) {
|
||||||
|
if (other.author != null)
|
||||||
|
author = other.author
|
||||||
|
|
||||||
|
if (other.artist != null)
|
||||||
|
artist = other.artist
|
||||||
|
|
||||||
|
if (other.description != null)
|
||||||
|
description = other.description
|
||||||
|
|
||||||
|
if (other.genre != null)
|
||||||
|
genre = other.genre
|
||||||
|
|
||||||
|
if (other.thumbnail_url != null)
|
||||||
|
thumbnail_url = other.thumbnail_url
|
||||||
|
|
||||||
|
status = other.status
|
||||||
|
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 sorting: Int
|
||||||
|
get() = chapter_flags and SORTING_MASK
|
||||||
|
set(sort) = setFlags(sort, SORTING_MASK)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val UNKNOWN = 0
|
||||||
|
const val ONGOING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val LICENSED = 3
|
||||||
|
|
||||||
|
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 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: Int): Manga = MangaImpl().apply {
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
|
||||||
|
url = pathUrl
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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: Int = 0
|
||||||
|
|
||||||
|
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,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface MangaSync : 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
|
||||||
|
|
||||||
|
var update: Boolean
|
||||||
|
|
||||||
|
fun copyPersonalFrom(other: MangaSync) {
|
||||||
|
last_chapter_read = other.last_chapter_read
|
||||||
|
score = other.score
|
||||||
|
status = other.status
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
|
||||||
|
sync_id = serviceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class MangaSyncImpl : MangaSync {
|
||||||
|
|
||||||
|
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 var update: Boolean = false
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val mangaSync = other as MangaSync
|
||||||
|
|
||||||
|
if (manga_id != mangaSync.manga_id) return false
|
||||||
|
if (sync_id != mangaSync.sync_id) return false
|
||||||
|
return remote_id == mangaSync.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,59 @@
|
|||||||
|
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.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 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 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,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
@ -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,81 @@
|
|||||||
|
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.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)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
open 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: Int) = 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 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()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||||
|
|
||||||
|
interface MangaSyncQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
||||||
|
.`object`(MangaSync::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
||||||
|
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
||||||
|
.whereArgs(manga.id, sync.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getMangasSync(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(MangaSync::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
|
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
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}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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(2).apply {
|
||||||
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
|
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
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
import android.database.Cursor
|
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.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
class LibraryMangaGetResolver : MangaGetResolver() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val INSTANCE = LibraryMangaGetResolver()
|
val INSTANCE = LibraryMangaGetResolver()
|
||||||
@ -15,10 +14,10 @@ class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
|||||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||||
val manga = super.mapFromCursor(cursor)
|
val manga = super.mapFromCursor(cursor)
|
||||||
|
|
||||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD)
|
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
||||||
manga.unread = cursor.getInt(unreadColumn)
|
manga.unread = cursor.getInt(unreadColumn)
|
||||||
|
|
||||||
val categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY)
|
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
|
||||||
manga.category = cursor.getInt(categoryColumn)
|
manga.category = cursor.getInt(categoryColumn)
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
|
@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.database.resolvers
|
|||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.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.MangaChapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
|
||||||
|
|
||||||
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||||
|
|
||||||
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val INSTANCE = MangaChapterGetResolver()
|
val INSTANCE = MangaChapterGetResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
|
private val mangaGetResolver = MangaGetResolver()
|
||||||
|
|
||||||
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver()
|
private val chapterGetResolver = ChapterGetResolver()
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"));
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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,49 @@
|
|||||||
|
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_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_READ 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"
|
||||||
|
|
||||||
|
}
|
@ -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"
|
|
||||||
+ ");";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
|
object MangaSyncTable {
|
||||||
|
|
||||||
|
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,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)"
|
||||||
|
}
|
@ -5,19 +5,18 @@ import android.net.Uri
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
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.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils
|
import eu.kanade.tachiyomi.util.*
|
||||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -25,13 +24,17 @@ import rx.schedulers.Schedulers
|
|||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.FileReader
|
import java.io.FileReader
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
|
class DownloadManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
) {
|
||||||
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
@ -39,6 +42,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
val runningSubject = BehaviorSubject.create<Boolean>()
|
val runningSubject = BehaviorSubject.create<Boolean>()
|
||||||
private var downloadsSubscription: Subscription? = null
|
private var downloadsSubscription: Subscription? = null
|
||||||
|
|
||||||
|
val downloadNotifier by lazy { DownloadNotifier(context) }
|
||||||
|
|
||||||
private val threadsSubject = BehaviorSubject.create<Int>()
|
private val threadsSubject = BehaviorSubject.create<Int>()
|
||||||
private var threadsSubscription: Subscription? = null
|
private var threadsSubscription: Subscription? = null
|
||||||
|
|
||||||
@ -48,27 +53,37 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
|
|
||||||
val PAGE_LIST_FILE = "index.json"
|
val PAGE_LIST_FILE = "index.json"
|
||||||
|
|
||||||
@Volatile private var isRunning: Boolean = false
|
@Volatile var isRunning: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
private fun initializeSubscriptions() {
|
private fun initializeSubscriptions() {
|
||||||
|
|
||||||
downloadsSubscription?.unsubscribe()
|
downloadsSubscription?.unsubscribe()
|
||||||
|
|
||||||
threadsSubscription = preferences.downloadThreads().asObservable()
|
threadsSubscription = preferences.downloadThreads().asObservable()
|
||||||
.subscribe { threadsSubject.onNext(it) }
|
.subscribe {
|
||||||
|
threadsSubject.onNext(it)
|
||||||
|
downloadNotifier.multipleDownloadThreads = it > 1
|
||||||
|
}
|
||||||
|
|
||||||
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
|
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
|
||||||
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.map { download -> areAllDownloadsFinished() }
|
.subscribe({
|
||||||
.subscribe({ finished ->
|
// Delete successful downloads from queue
|
||||||
if (finished!!) {
|
if (it.status == Download.DOWNLOADED) {
|
||||||
|
// remove downloaded chapter from queue
|
||||||
|
queue.del(it)
|
||||||
|
downloadNotifier.onProgressChange(queue)
|
||||||
|
}
|
||||||
|
if (areAllDownloadsFinished()) {
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { error ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(e, e.message)
|
Timber.e(error)
|
||||||
context.toast(e.message)
|
downloadNotifier.onError(error.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
@ -94,16 +109,18 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a download object for every chapter in the event and add them to the downloads queue
|
// Create a download object for every chapter and add them to the downloads queue
|
||||||
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
val manga = event.manga
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
|
||||||
val source = sourceManager.get(manga.source)
|
|
||||||
|
// Add chapters to queue from the start
|
||||||
|
val sortedChapters = chapters.sortedByDescending { it.source_order }
|
||||||
|
|
||||||
// Used to avoid downloading chapters with the same name
|
// Used to avoid downloading chapters with the same name
|
||||||
val addedChapters = ArrayList<String>()
|
val addedChapters = ArrayList<String>()
|
||||||
val pending = ArrayList<Download>()
|
val pending = ArrayList<Download>()
|
||||||
|
|
||||||
for (chapter in event.chapters) {
|
for (chapter in sortedChapters) {
|
||||||
if (addedChapters.contains(chapter.name))
|
if (addedChapters.contains(chapter.name))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -115,6 +132,12 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
pending.add(download)
|
pending.add(download)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize queue size
|
||||||
|
downloadNotifier.initialQueueSize = queue.size
|
||||||
|
// Show notification
|
||||||
|
downloadNotifier.onProgressChange(queue)
|
||||||
|
|
||||||
if (isRunning) downloadsQueueSubject.onNext(pending)
|
if (isRunning) downloadsQueueSubject.onNext(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,35 +187,41 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
private fun downloadChapter(download: Download): Observable<Download> {
|
private fun downloadChapter(download: Download): Observable<Download> {
|
||||||
DiskUtils.createDirectory(download.directory)
|
DiskUtils.createDirectory(download.directory)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null)
|
val pageListObservable: Observable<List<Page>> = if (download.pages == null)
|
||||||
// Pull page list from network and add them to download object
|
// Pull page list from network and add them to download object
|
||||||
download.source.pullPageListFromNetwork(download.chapter.url)
|
download.source.fetchPageListFromNetwork(download.chapter)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
download.pages = pages
|
download.pages = pages
|
||||||
savePageList(download)
|
savePageList(download)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
// Or if the page list already exists, start from the file
|
// Or if the page list already exists, start from the file
|
||||||
Observable.just(download.pages)
|
Observable.just(download.pages)
|
||||||
|
|
||||||
return Observable.defer<Download> { pageListObservable
|
return Observable.defer {
|
||||||
.doOnNext { pages ->
|
pageListObservable
|
||||||
download.downloadedImages = 0
|
.doOnNext { pages ->
|
||||||
download.status = Download.DOWNLOADING
|
download.downloadedImages = 0
|
||||||
}
|
download.status = Download.DOWNLOADING
|
||||||
// Get all the URLs to the source images, fetch pages if necessary
|
}
|
||||||
.flatMap { download.source.getAllImageUrlsFromPageList(it) }
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
// Start downloading images, consider we can have downloaded images already
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
.concatMap { page -> getOrDownloadImage(page, download) }
|
// Start downloading images, consider we can have downloaded images already
|
||||||
// Do after download completes
|
.concatMap { page -> getOrDownloadImage(page, download) }
|
||||||
.doOnCompleted { onDownloadCompleted(download) }
|
// Do when page is downloaded.
|
||||||
.toList()
|
.doOnNext {
|
||||||
.map { pages -> download }
|
downloadNotifier.onProgressChange(download, queue)
|
||||||
// If the page list threw, it will resume here
|
}
|
||||||
.onErrorResumeNext { error ->
|
// Do after download completes
|
||||||
download.status = Download.ERROR
|
.doOnCompleted { onDownloadCompleted(download) }
|
||||||
Observable.just(download)
|
.toList()
|
||||||
}
|
.map { pages -> download }
|
||||||
|
// If the page list threw, it will resume here
|
||||||
|
.onErrorResumeNext { error ->
|
||||||
|
download.status = Download.ERROR
|
||||||
|
downloadNotifier.onError(error.message, download.chapter.name)
|
||||||
|
Observable.just(download)
|
||||||
|
}
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,13 +257,23 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save image on disk
|
// Save image on disk
|
||||||
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
|
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
return source.getImageProgressResponse(page)
|
return source.imageResponse(page)
|
||||||
.flatMap({ resp ->
|
.map {
|
||||||
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename)
|
val file = File(directory, filename)
|
||||||
Observable.just(page)
|
try {
|
||||||
}).retry(2)
|
file.parentFile.mkdirs()
|
||||||
|
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
it.close()
|
||||||
|
file.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
page
|
||||||
|
}
|
||||||
|
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||||
|
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
||||||
@ -285,20 +324,20 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
var actualProgress = 0
|
var actualProgress = 0
|
||||||
var status = Download.DOWNLOADED
|
var status = Download.DOWNLOADED
|
||||||
// If any page has an error, the download result will be error
|
// If any page has an error, the download result will be error
|
||||||
for (page in download.pages) {
|
for (page in download.pages!!) {
|
||||||
actualProgress += page.progress
|
actualProgress += page.progress
|
||||||
if (page.status != Page.READY) status = Download.ERROR
|
if (page.status != Page.READY) {
|
||||||
|
status = Download.ERROR
|
||||||
|
downloadNotifier.onError(context.getString(R.string.download_notifier_page_ready_error), download.chapter.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Ensure that the chapter folder has all the images
|
// Ensure that the chapter folder has all the images
|
||||||
if (!isChapterDownloaded(download.directory, download.pages)) {
|
if (!isChapterDownloaded(download.directory, download.pages)) {
|
||||||
status = Download.ERROR
|
status = Download.ERROR
|
||||||
|
downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name)
|
||||||
}
|
}
|
||||||
download.totalProgress = actualProgress
|
download.totalProgress = actualProgress
|
||||||
download.status = status
|
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
|
// Return the page list from the chapter's directory if it exists, null otherwise
|
||||||
@ -306,26 +345,14 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||||
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
||||||
|
|
||||||
var reader: JsonReader? = null
|
return try {
|
||||||
try {
|
JsonReader(FileReader(pagesFile)).use {
|
||||||
if (pagesFile.exists()) {
|
val collectionType = object : TypeToken<List<Page>>() {}.type
|
||||||
reader = JsonReader(FileReader(pagesFile.absolutePath))
|
gson.fromJson(it, collectionType)
|
||||||
val collectionType = object : TypeToken<List<Page>>() {
|
|
||||||
|
|
||||||
}.type
|
|
||||||
return gson.fromJson<List<Page>>(reader, collectionType)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e.cause, e.message)
|
null
|
||||||
} finally {
|
|
||||||
if (reader != null) try {
|
|
||||||
reader.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
/* Do nothing */
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shortcut for the method above
|
// Shortcut for the method above
|
||||||
@ -338,34 +365,27 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
|
||||||
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
|
||||||
|
|
||||||
var out: FileOutputStream? = null
|
pagesFile.outputStream().use {
|
||||||
try {
|
try {
|
||||||
out = FileOutputStream(pagesFile)
|
it.write(gson.toJson(pages).toByteArray())
|
||||||
out.write(gson.toJson(pages).toByteArray())
|
it.flush()
|
||||||
out.flush()
|
} catch (error: Exception) {
|
||||||
} catch (e: IOException) {
|
Timber.e(error)
|
||||||
Timber.e(e.cause, e.message)
|
|
||||||
} finally {
|
|
||||||
if (out != null) try {
|
|
||||||
out.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
/* Do nothing */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shortcut for the method above
|
// Shortcut for the method above
|
||||||
private fun savePageList(download: Download) {
|
private fun savePageList(download: Download) {
|
||||||
savePageList(download.source, download.manga, download.chapter, download.pages)
|
savePageList(download.source, download.manga, download.chapter, download.pages!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
||||||
val mangaRelativePath = source.visibleName +
|
val mangaRelativePath = source.toString() +
|
||||||
File.separator +
|
File.separator +
|
||||||
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||||
|
|
||||||
return File(preferences.downloadsDirectory, mangaRelativePath)
|
return File(preferences.downloadsDirectory().getOrDefault(), mangaRelativePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the absolute path to the chapter directory
|
// Get the absolute path to the chapter directory
|
||||||
@ -412,13 +432,19 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
return !pending.isEmpty()
|
return !pending.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopDownloads() {
|
fun stopDownloads(errorMessage: String? = null) {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
for (download in queue) {
|
for (download in queue) {
|
||||||
if (download.status == Download.DOWNLOADING) {
|
if (download.status == Download.DOWNLOADING) {
|
||||||
download.status = Download.ERROR
|
download.status = Download.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
errorMessage?.let { downloadNotifier.onError(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearQueue() {
|
||||||
|
queue.clear()
|
||||||
|
downloadNotifier.onClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.util.notificationManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
*/
|
||||||
|
class DownloadNotifier(private val context: Context) {
|
||||||
|
/**
|
||||||
|
* Notification builder.
|
||||||
|
*/
|
||||||
|
private val notificationBuilder = NotificationCompat.Builder(context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the notification.
|
||||||
|
*/
|
||||||
|
private val notificationId: Int
|
||||||
|
get() = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of download. Used for correct notification icon.
|
||||||
|
*/
|
||||||
|
private var isDownloading = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of queue on start download.
|
||||||
|
*/
|
||||||
|
internal var initialQueueSize = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simultaneous download setting > 1.
|
||||||
|
*/
|
||||||
|
internal var multipleDownloadThreads = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when download progress changes.
|
||||||
|
* Note: Only accepted when multi download active.
|
||||||
|
*
|
||||||
|
* @param queue the queue containing downloads.
|
||||||
|
*/
|
||||||
|
internal 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
|
||||||
|
*/
|
||||||
|
internal 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) {
|
||||||
|
// Check if download is completed
|
||||||
|
if (multipleDownloadThreads) {
|
||||||
|
if (queue.isEmpty()) {
|
||||||
|
onComplete(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (download != null && download.pages!!.size == download.downloadedImages) {
|
||||||
|
onComplete(download)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create notification
|
||||||
|
with (notificationBuilder) {
|
||||||
|
// Check if icon needs refresh
|
||||||
|
if (!isDownloading) {
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
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 {
|
||||||
|
if (it.chapter.name.length >= 33)
|
||||||
|
setContentTitle(it.chapter.name.slice(IntRange(0, 30)).plus("..."))
|
||||||
|
else
|
||||||
|
setContentTitle(it.chapter.name)
|
||||||
|
|
||||||
|
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
|
||||||
|
context.notificationManager.notify(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when chapter is downloaded
|
||||||
|
*
|
||||||
|
* @param download download object containing download information
|
||||||
|
*/
|
||||||
|
private fun onComplete(download: Download?) {
|
||||||
|
// Create notification.
|
||||||
|
with(notificationBuilder) {
|
||||||
|
setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification.
|
||||||
|
context.notificationManager.notify(notificationId, notificationBuilder.build())
|
||||||
|
|
||||||
|
// Reset initial values
|
||||||
|
isDownloading = false
|
||||||
|
initialQueueSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the notification message
|
||||||
|
*/
|
||||||
|
internal fun onClear() {
|
||||||
|
context.notificationManager.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on error while downloading chapter
|
||||||
|
*
|
||||||
|
* @param error string containing error information
|
||||||
|
* @param chapter string containing chapter title
|
||||||
|
*/
|
||||||
|
internal fun onError(error: String? = null, chapter: String? = null) {
|
||||||
|
// Create notification
|
||||||
|
with(notificationBuilder) {
|
||||||
|
setContentTitle(chapter ?: context.getString(R.string.download_notifier_title_error))
|
||||||
|
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
|
||||||
|
|
||||||
|
// Reset download information
|
||||||
|
onClear()
|
||||||
|
isDownloading = false
|
||||||
|
}
|
||||||
|
}
|
@ -7,14 +7,13 @@ import android.os.IBinder
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
|
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import javax.inject.Inject
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class DownloadService : Service() {
|
class DownloadService : Service() {
|
||||||
|
|
||||||
@ -29,8 +28,8 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var downloadManager: DownloadManager
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private var networkChangeSubscription: Subscription? = null
|
private var networkChangeSubscription: Subscription? = null
|
||||||
@ -39,7 +38,6 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
App.get(this).component.inject(this)
|
|
||||||
|
|
||||||
createWakeLock()
|
createWakeLock()
|
||||||
|
|
||||||
@ -82,12 +80,12 @@ class DownloadService : Service() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
} else if (isRunning) {
|
} else if (isRunning) {
|
||||||
downloadManager.stopDownloads()
|
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
downloadManager.stopDownloads()
|
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,40 @@
|
|||||||
|
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.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
|
||||||
|
|
||||||
|
lateinit var directory: File
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -4,36 +4,43 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.util.*
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class DownloadQueue : ArrayList<Download>() {
|
class DownloadQueue(private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
|
||||||
|
: List<Download> by queue {
|
||||||
|
|
||||||
private val statusSubject = PublishSubject.create<Download>()
|
private val statusSubject = PublishSubject.create<Download>()
|
||||||
|
|
||||||
override fun add(download: Download): Boolean {
|
private val removeSubject = PublishSubject.create<Download>()
|
||||||
|
|
||||||
|
fun add(download: Download): Boolean {
|
||||||
download.setStatusSubject(statusSubject)
|
download.setStatusSubject(statusSubject)
|
||||||
download.status = Download.QUEUE
|
download.status = Download.QUEUE
|
||||||
return super.add(download)
|
return queue.add(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun del(download: Download) {
|
fun del(download: Download) {
|
||||||
super.remove(download)
|
val removed = queue.remove(download)
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
}
|
if (removed) {
|
||||||
|
removeSubject.onNext(download)
|
||||||
fun del(chapter: Chapter) {
|
|
||||||
for (download in this) {
|
|
||||||
if (download.chapter.id == chapter.id) {
|
|
||||||
del(download)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveDownloads() =
|
fun del(chapter: Chapter) {
|
||||||
|
find { it.chapter.id == chapter.id }?.let { del(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
queue.forEach { del(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActiveDownloads(): Observable<Download> =
|
||||||
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
||||||
|
|
||||||
fun getStatusObservable() = statusSubject.onBackpressureBuffer()
|
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||||
|
|
||||||
|
fun getRemovedObservable(): Observable<Download> = removeSubject.onBackpressureBuffer()
|
||||||
|
|
||||||
fun getProgressObservable(): Observable<Download> {
|
fun getProgressObservable(): Observable<Download> {
|
||||||
return statusSubject.onBackpressureBuffer()
|
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.data.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,82 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
|
||||||
|
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
|
||||||
|
* fallbacks to network and copies it to the cache.
|
||||||
|
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
|
||||||
|
* to network for fetching.
|
||||||
|
*
|
||||||
|
* @param networkFetcher the network fetcher for this cover.
|
||||||
|
* @param file the file where this cover should be. It may exists or not.
|
||||||
|
* @param manga the manga of the cover to load.
|
||||||
|
*/
|
||||||
|
class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||||
|
private val file: File,
|
||||||
|
private val manga: Manga)
|
||||||
|
: DataFetcher<InputStream> {
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun loadData(priority: Priority): InputStream? {
|
||||||
|
if (manga.favorite) {
|
||||||
|
synchronized(file) {
|
||||||
|
if (!file.exists()) {
|
||||||
|
val tmpFile = File(file.path + ".tmp")
|
||||||
|
try {
|
||||||
|
// Retrieve source stream.
|
||||||
|
val input = networkFetcher.loadData(priority)
|
||||||
|
?: throw Exception("Couldn't open source stream")
|
||||||
|
|
||||||
|
// Retrieve destination stream, create parent folders if needed.
|
||||||
|
val output = try {
|
||||||
|
tmpFile.outputStream()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
tmpFile.parentFile.mkdirs()
|
||||||
|
tmpFile.outputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file and rename to the original.
|
||||||
|
input.use { output.use { input.copyTo(output) } }
|
||||||
|
tmpFile.renameTo(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file.inputStream()
|
||||||
|
} else {
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
return networkFetcher.loadData(priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
networkFetcher.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanup() {
|
||||||
|
networkFetcher.cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.LruCache
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
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.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
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 [MangaDataFetcher], 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.
|
||||||
|
*/
|
||||||
|
val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source manager.
|
||||||
|
*/
|
||||||
|
val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base network loader.
|
||||||
|
*/
|
||||||
|
private val baseLoader = 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<Int, 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 [MangaDataFetcher] 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.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)), coverCache.getCoverFile(url!!)).apply {
|
||||||
|
lruCache.put(url, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the network fetcher for this request url.
|
||||||
|
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
|
||||||
|
|
||||||
|
// Return an instance of our fetcher providing the needed elements.
|
||||||
|
return MangaDataFetcher(networkFetcher, file, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
||||||
|
*
|
||||||
|
* @param manga the model.
|
||||||
|
*/
|
||||||
|
fun getHeaders(manga: Manga): Headers {
|
||||||
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
|
||||||
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.SystemClock
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.util.alarmManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used to update the library by firing an alarm after a specified time.
|
|
||||||
* It has a receiver reacting to system's boot and the intent fired by this alarm.
|
|
||||||
* See [onReceive] for more information.
|
|
||||||
*/
|
|
||||||
class LibraryUpdateAlarm : BroadcastReceiver() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the alarm to run the intent that updates the library.
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intervalInHours the time in hours when it will be executed. Defaults to the
|
|
||||||
* value stored in preferences.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun startAlarm(context: Context,
|
|
||||||
intervalInHours: Int = PreferencesHelper.getLibraryUpdateInterval(context)) {
|
|
||||||
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
|
|
||||||
stopAlarm(context)
|
|
||||||
if (intervalInHours == 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Get the time the alarm should fire the event to update.
|
|
||||||
val intervalInMillis = intervalInHours * 60 * 60 * 1000
|
|
||||||
val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
|
|
||||||
|
|
||||||
// Start the alarm.
|
|
||||||
val pendingIntent = getPendingIntent(context)
|
|
||||||
context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
||||||
nextRun, intervalInMillis.toLong(), pendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the alarm if it's running.
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
fun stopAlarm(context: Context) {
|
|
||||||
val pendingIntent = getPendingIntent(context)
|
|
||||||
context.alarmManager.cancel(pendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the intent the alarm should run when it's fired.
|
|
||||||
* @param context the application context.
|
|
||||||
* @return the intent that will run when the alarm is fired.
|
|
||||||
*/
|
|
||||||
private fun getPendingIntent(context: Context): PendingIntent {
|
|
||||||
val intent = Intent(context, LibraryUpdateAlarm::class.java)
|
|
||||||
intent.action = LIBRARY_UPDATE_ACTION
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the intents received by this [BroadcastReceiver].
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intent the intent to process.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.action) {
|
|
||||||
// Start the alarm when the system is booted.
|
|
||||||
Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
|
|
||||||
// Update the library when the alarm fires an event.
|
|
||||||
LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -5,52 +5,32 @@ import android.app.Service
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import android.util.Pair
|
import eu.kanade.tachiyomi.Constants
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||||
import eu.kanade.tachiyomi.util.DeviceUtil
|
|
||||||
import eu.kanade.tachiyomi.util.notification
|
import eu.kanade.tachiyomi.util.notification
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
// Intent key for forced library update
|
|
||||||
val UPDATE_IS_FORCED = "is_forced"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the start intent for [LibraryUpdateService].
|
|
||||||
* @param context the application context.
|
|
||||||
* @param isForced true when forcing library update
|
|
||||||
* @return the intent of the service.
|
|
||||||
*/
|
|
||||||
fun getIntent(context: Context, isForced: Boolean = false): Intent {
|
|
||||||
return Intent(context, LibraryUpdateService::class.java).apply {
|
|
||||||
putExtra(UPDATE_IS_FORCED, isForced)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will take care of updating the chapters of the manga from the library. It can be
|
* This class will take care of updating the chapters of the manga from the library. It can be
|
||||||
@ -62,35 +42,86 @@ fun isRunning(context: Context): Boolean {
|
|||||||
*/
|
*/
|
||||||
class LibraryUpdateService : Service() {
|
class LibraryUpdateService : Service() {
|
||||||
|
|
||||||
// Dependencies injected through dagger.
|
/**
|
||||||
@Inject lateinit var db: DatabaseHelper
|
* Database helper.
|
||||||
@Inject lateinit var sourceManager: SourceManager
|
*/
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
// Wake lock that will be held until the service is destroyed.
|
/**
|
||||||
|
* Source manager.
|
||||||
|
*/
|
||||||
|
val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences.
|
||||||
|
*/
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
// Subscription where the update is done.
|
/**
|
||||||
|
* Subscription where the update is done.
|
||||||
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the library update notification.
|
||||||
|
*/
|
||||||
|
private val notificationId: Int
|
||||||
|
get() = Constants.NOTIFICATION_LIBRARY_ID
|
||||||
|
|
||||||
|
private var notificationBitmap: Bitmap? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val UPDATE_NOTIFICATION_ID = 1
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static method to start the service. It will be started only if there isn't another
|
* Key for category to update.
|
||||||
* instance already running.
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
const val UPDATE_CATEGORY = "category"
|
||||||
fun start(context: Context, isForced: Boolean = false) {
|
|
||||||
|
/**
|
||||||
|
* Key for updating the details instead of the chapters.
|
||||||
|
*/
|
||||||
|
const val UPDATE_DETAILS = "details"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the service. It will be started only if there isn't another instance already
|
||||||
|
* running.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @param category a specific category to update, or null for global update.
|
||||||
|
* @param details whether to update the details instead of the list of chapters.
|
||||||
|
*/
|
||||||
|
fun start(context: Context, category: Category? = null, details: Boolean = false) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
context.startService(getIntent(context, isForced))
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
|
putExtra(UPDATE_DETAILS, details)
|
||||||
|
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(getIntent(context))
|
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -101,17 +132,17 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
App.get(this).component.inject(this)
|
|
||||||
createAndAcquireWakeLock()
|
createAndAcquireWakeLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is destroyed. It destroy the running subscription, resets
|
* Method called when the service is destroyed. It destroys the running subscription, resets
|
||||||
* the alarm and release the wake lock.
|
* the alarm and release the wake lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
LibraryUpdateAlarm.startAlarm(this)
|
notificationBitmap?.recycle()
|
||||||
|
notificationBitmap = null
|
||||||
destroyWakeLock()
|
destroyWakeLock()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@ -123,91 +154,97 @@ class LibraryUpdateService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service receives an intent. In this case, the content of the intent
|
* Method called when the service receives an intent.
|
||||||
* is irrelevant, because everything required is fetched in [updateLibrary].
|
*
|
||||||
* @param intent the intent from [start].
|
* @param intent the start intent from.
|
||||||
* @param flags the flags of the command.
|
* @param flags the flags of the command.
|
||||||
* @param startId the start id of this command.
|
* @param startId the start id of this command.
|
||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
// If there's no network available, set a component to start this service again when
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
// a connection is available.
|
|
||||||
if (!DeviceUtil.isNetworkConnected(this)) {
|
|
||||||
Timber.i("Sync canceled, connection not available")
|
|
||||||
showWarningNotification(getString(R.string.notification_no_connection_title),
|
|
||||||
getString(R.string.notification_no_connection_body))
|
|
||||||
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
|
|
||||||
stopSelf(startId)
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user doesn't want to update while phone is not charging, cancel sync
|
|
||||||
else if (preferences.updateOnlyWhenCharging() && !(intent?.getBooleanExtra(UPDATE_IS_FORCED, false) ?: false) && !DeviceUtil.isPowerConnected(this)) {
|
|
||||||
Timber.i("Sync canceled, not connected to ac power")
|
|
||||||
// Create force library update intent
|
|
||||||
val forceIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.FORCE_LIBRARY_UPDATE)
|
|
||||||
// Show warning
|
|
||||||
showWarningNotification(getString(R.string.notification_not_connected_to_ac_title),
|
|
||||||
getString(R.string.notification_not_connected_to_ac_body), forceIntent)
|
|
||||||
stopSelf(startId)
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable.defer { updateLibrary() }
|
subscription = Observable
|
||||||
|
.defer {
|
||||||
|
if (notificationBitmap == null) {
|
||||||
|
notificationBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = getMangaToUpdate(intent)
|
||||||
|
|
||||||
|
// Update either chapter list or manga details.
|
||||||
|
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
|
||||||
|
updateChapterList(mangaList)
|
||||||
|
else
|
||||||
|
updateDetails(mangaList)
|
||||||
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({},
|
.subscribe({
|
||||||
{
|
}, {
|
||||||
showNotification(getString(R.string.notification_update_error), "")
|
showNotification(getString(R.string.notification_update_error), "")
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.START_STICKY
|
return Service.START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a PendingIntent for LibraryUpdate broadcast class
|
* Returns the list of manga to be updated.
|
||||||
* @param action id of action
|
*
|
||||||
|
* @param intent the update intent.
|
||||||
|
* @return a list of manga to update
|
||||||
*/
|
*/
|
||||||
fun getLibraryUpdateReceiverIntent(action: String): PendingIntent {
|
fun getMangaToUpdate(intent: Intent): List<Manga> {
|
||||||
return PendingIntent.getBroadcast(this, 0,
|
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
|
||||||
Intent(this, LibraryUpdateReceiver::class.java).apply { this.action = action }, 0)
|
|
||||||
|
var listToUpdate = if (categoryId != -1)
|
||||||
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
|
else {
|
||||||
|
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
|
||||||
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
|
.filter { it.category in categoriesToUpdate }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
else
|
||||||
|
db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
||||||
|
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
|
||||||
|
}
|
||||||
|
|
||||||
|
return listToUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that updates the library. It's called in a background thread, so it's safe to do
|
* Method that updates the given list of manga. It's called in a background thread, so it's safe
|
||||||
* heavy operations or network calls here.
|
* to do heavy operations or network calls here.
|
||||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||||
* progress.
|
* progress.
|
||||||
|
*
|
||||||
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
fun updateLibrary(): Observable<Manga> {
|
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Manga>()
|
||||||
val failedUpdates = ArrayList<Manga>()
|
val failedUpdates = ArrayList<Manga>()
|
||||||
|
|
||||||
val cancelIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.CANCEL_LIBRARY_UPDATE)
|
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
||||||
|
Intent(this, CancelUpdateReceiver::class.java), 0)
|
||||||
// Get the manga list that is going to be updated.
|
|
||||||
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
|
|
||||||
val toUpdate = if (!preferences.updateOnlyNonCompleted())
|
|
||||||
allLibraryMangas
|
|
||||||
else
|
|
||||||
allLibraryMangas.filter { it.status != Manga.COMPLETED }
|
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(toUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, toUpdate.size, cancelIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
@ -230,23 +267,62 @@ class LibraryUpdateService : Service() {
|
|||||||
} else {
|
} else {
|
||||||
showResultNotification(newUpdates, failedUpdates)
|
showResultNotification(newUpdates, failedUpdates)
|
||||||
}
|
}
|
||||||
|
LibraryUpdateTrigger.setupTask(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the chapters for the given manga and adds them to the database.
|
* Updates the chapters for the given manga and adds them to the database.
|
||||||
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
||||||
val source = sourceManager.get(manga.source)
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
||||||
return source!!
|
return source.fetchChapterList(manga)
|
||||||
.pullChaptersFromNetwork(manga.url)
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
.flatMap { db.insertOrRemoveChapters(manga, it, source) }
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that updates the details of the given list of manga. It's called in a background
|
||||||
|
* thread, so it's safe to do heavy operations or network calls here.
|
||||||
|
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||||
|
* progress.
|
||||||
|
*
|
||||||
|
* @param mangaToUpdate the list to update
|
||||||
|
* @return an observable delivering the progress of each update.
|
||||||
|
*/
|
||||||
|
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||||
|
// Initialize the variables holding the progress of the updates.
|
||||||
|
val count = AtomicInteger(0)
|
||||||
|
|
||||||
|
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
||||||
|
Intent(this, CancelUpdateReceiver::class.java), 0)
|
||||||
|
|
||||||
|
// Emit each manga and update it sequentially.
|
||||||
|
return Observable.from(mangaToUpdate)
|
||||||
|
// Notify manga that will update.
|
||||||
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
|
||||||
|
// Update the details of the manga.
|
||||||
|
.concatMap { manga ->
|
||||||
|
val source = sourceManager.get(manga.source) as? OnlineSource
|
||||||
|
?: return@concatMap Observable.empty<Manga>()
|
||||||
|
|
||||||
|
source.fetchMangaDetails(manga)
|
||||||
|
.doOnNext { networkManga ->
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
.onErrorReturn { manga }
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
cancelNotification()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text that will be displayed in the notification when there are new chapters.
|
* Returns the text that will be displayed in the notification when there are new chapters.
|
||||||
|
*
|
||||||
* @param updates a list of manga that contains new chapters.
|
* @param updates a list of manga that contains new chapters.
|
||||||
* @param failedUpdates a list of manga that failed to update.
|
* @param failedUpdates a list of manga that failed to update.
|
||||||
* @return the body of the notification to display.
|
* @return the body of the notification to display.
|
||||||
@ -295,57 +371,41 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification with the given title and body.
|
* Shows the notification with the given title and body.
|
||||||
|
*
|
||||||
* @param title the title of the notification.
|
* @param title the title of the notification.
|
||||||
* @param body the body of the notification.
|
* @param body the body of the notification.
|
||||||
*/
|
*/
|
||||||
private fun showNotification(title: String, body: String) {
|
private fun showNotification(title: String, body: String) {
|
||||||
val n = notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setContentText(body)
|
setContentText(body)
|
||||||
}
|
})
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
|
*
|
||||||
* @param manga the manga that's being updated.
|
* @param manga the manga that's being updated.
|
||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
||||||
val n = notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(manga.title)
|
setContentTitle(manga.title)
|
||||||
setProgress(total, current, false)
|
setProgress(total, current, false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||||
}
|
})
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show warning message when library can't be updated
|
|
||||||
* @param warningTitle title of warning
|
|
||||||
* @param warningBody warning information
|
|
||||||
* @param pendingIntent Intent called when action clicked
|
|
||||||
*/
|
|
||||||
private fun showWarningNotification(warningTitle: String, warningBody: String, pendingIntent: PendingIntent? = null) {
|
|
||||||
val n = notification() {
|
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp_img)
|
|
||||||
setContentTitle(warningTitle)
|
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(warningBody))
|
|
||||||
setContentIntent(notificationIntent)
|
|
||||||
if (pendingIntent != null) {
|
|
||||||
addAction(R.drawable.ic_refresh_grey_24dp_img, getString(R.string.action_force), pendingIntent)
|
|
||||||
}
|
|
||||||
setAutoCancel(true)
|
|
||||||
}
|
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the result of the update done by the service.
|
* Shows the notification containing the result of the update done by the service.
|
||||||
|
*
|
||||||
* @param updates a list of manga with new updates.
|
* @param updates a list of manga with new updates.
|
||||||
* @param failed a list of manga that failed to update.
|
* @param failed a list of manga that failed to update.
|
||||||
*/
|
*/
|
||||||
@ -353,21 +413,21 @@ class LibraryUpdateService : Service() {
|
|||||||
val title = getString(R.string.notification_update_completed)
|
val title = getString(R.string.notification_update_completed)
|
||||||
val body = getUpdatedMangasBody(updates, failed)
|
val body = getUpdatedMangasBody(updates, failed)
|
||||||
|
|
||||||
val n = notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||||
setContentIntent(notificationIntent)
|
setContentIntent(notificationIntent)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
}
|
})
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the notification.
|
* Cancels the notification.
|
||||||
*/
|
*/
|
||||||
private fun cancelNotification() {
|
private fun cancelNotification() {
|
||||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
notificationManager.cancel(notificationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -381,49 +441,17 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that triggers the library to update when a connection is available. It receives
|
* Class that stops updating the library.
|
||||||
* network changes.
|
|
||||||
*/
|
*/
|
||||||
class SyncOnConnectionAvailable : BroadcastReceiver() {
|
class CancelUpdateReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called when a network change occurs.
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intent the intent received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (DeviceUtil.isNetworkConnected(context)) {
|
|
||||||
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
|
|
||||||
context.startService(getIntent(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that triggers the library to update.
|
|
||||||
*/
|
|
||||||
class LibraryUpdateReceiver : BroadcastReceiver() {
|
|
||||||
companion object {
|
|
||||||
// Cancel library update action
|
|
||||||
val CANCEL_LIBRARY_UPDATE = "eu.kanade.CANCEL_LIBRARY_UPDATE"
|
|
||||||
// Force library update
|
|
||||||
val FORCE_LIBRARY_UPDATE = "eu.kanade.FORCE_LIBRARY_UPDATE"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when user wants a library update.
|
* Method called when user wants a library update.
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param intent the intent received.
|
* @param intent the intent received.
|
||||||
*/
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
LibraryUpdateService.stop(context)
|
||||||
CANCEL_LIBRARY_UPDATE -> {
|
context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID)
|
||||||
LibraryUpdateService.stop(context)
|
|
||||||
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
|
||||||
}
|
|
||||||
FORCE_LIBRARY_UPDATE -> LibraryUpdateService.start(context, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.android.gms.gcm.*
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class LibraryUpdateTrigger : GcmTaskService() {
|
||||||
|
|
||||||
|
override fun onInitializeTasks() {
|
||||||
|
setupTask(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRunTask(params: TaskParams): Int {
|
||||||
|
LibraryUpdateService.start(this)
|
||||||
|
return GcmNetworkManager.RESULT_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault()
|
||||||
|
if (interval > 0) {
|
||||||
|
val restrictions = preferences.libraryUpdateRestriction()
|
||||||
|
val acRestriction = "ac" in restrictions
|
||||||
|
val wifiRestriction = if ("wifi" in restrictions)
|
||||||
|
Task.NETWORK_STATE_UNMETERED
|
||||||
|
else
|
||||||
|
Task.NETWORK_STATE_ANY
|
||||||
|
|
||||||
|
val task = PeriodicTask.Builder()
|
||||||
|
.setService(LibraryUpdateTrigger::class.java)
|
||||||
|
.setTag("Library periodic update")
|
||||||
|
.setPeriod(interval * 60 * 60L)
|
||||||
|
.setFlex(5 * 60)
|
||||||
|
.setRequiredNetwork(wifiRestriction)
|
||||||
|
.setRequiresCharging(acRestriction)
|
||||||
|
.setUpdateCurrent(true)
|
||||||
|
.setPersisted(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
GcmNetworkManager.getInstance(context).schedule(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTask(context: Context) {
|
||||||
|
GcmNetworkManager.getInstance(context).cancelAllTasks(LibraryUpdateTrigger::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
package eu.kanade.tachiyomi.data.mangasync
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList
|
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
||||||
|
|
||||||
class MangaSyncManager(private val context: Context) {
|
class MangaSyncManager(private val context: Context) {
|
||||||
|
|
||||||
val services: List<MangaSyncService>
|
|
||||||
val myAnimeList: MyAnimeList
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
|
const val ANILIST = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
myAnimeList = MyAnimeList(context, MYANIMELIST)
|
|
||||||
services = listOf(myAnimeList)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getService(id: Int): MangaSyncService = services.find { it.id == id }!!
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
|
// TODO enable anilist
|
||||||
|
val services = listOf(myAnimeList)
|
||||||
|
|
||||||
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
||||||
|
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
val networkService: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
open val client: OkHttpClient
|
||||||
|
get() = networkService.client
|
||||||
|
|
||||||
|
// Name of the manga sync service to display
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
abstract fun login(username: String, password: String): Completable
|
||||||
|
|
||||||
|
open val isLogged: Boolean
|
||||||
|
get() = !getUsername().isEmpty() &&
|
||||||
|
!getPassword().isEmpty()
|
||||||
|
|
||||||
|
abstract fun add(manga: MangaSync): Observable<MangaSync>
|
||||||
|
|
||||||
|
abstract fun update(manga: MangaSync): Observable<MangaSync>
|
||||||
|
|
||||||
|
abstract fun bind(manga: MangaSync): Observable<MangaSync>
|
||||||
|
|
||||||
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
|
fun saveCredentials(username: String, password: String) {
|
||||||
|
preferences.setMangaSyncCredentials(this, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
open fun logout() {
|
||||||
|
preferences.setMangaSyncCredentials(this, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUsername() = preferences.mangaSyncUsername(this)
|
||||||
|
|
||||||
|
fun getPassword() = preferences.mangaSyncPassword(this)
|
||||||
|
|
||||||
|
}
|
@ -4,25 +4,23 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import javax.inject.Inject
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class UpdateMangaSyncService : Service() {
|
class UpdateMangaSyncService : Service() {
|
||||||
|
|
||||||
@Inject lateinit var syncManager: MangaSyncManager
|
val syncManager: MangaSyncManager by injectLazy()
|
||||||
@Inject lateinit var db: DatabaseHelper
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
private lateinit var subscriptions: CompositeSubscription
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
App.get(this).component.inject(this)
|
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,15 +46,13 @@ class UpdateMangaSyncService : Service() {
|
|||||||
|
|
||||||
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
||||||
val sync = syncManager.getService(mangaSync.sync_id)
|
val sync = syncManager.getService(mangaSync.sync_id)
|
||||||
|
if (sync == null) {
|
||||||
|
stopSelf(startId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
||||||
.flatMap {
|
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
||||||
if (it.isSuccessful) {
|
|
||||||
db.insertMangaSync(mangaSync).asRxObservable()
|
|
||||||
} else {
|
|
||||||
Observable.error(Exception("Could not update manga in remote service"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ stopSelf(startId) },
|
.subscribe({ stopSelf(startId) },
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 5
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "AniList"
|
||||||
|
|
||||||
|
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||||
|
|
||||||
|
private val api by lazy {
|
||||||
|
AnilistApi.createService(networkService.client.newBuilder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(authCode: String): Completable {
|
||||||
|
// Create a new api with the default client to avoid request interceptions.
|
||||||
|
return AnilistApi.createService(client)
|
||||||
|
// Request the access token from the API with the authorization code.
|
||||||
|
.requestAccessToken(authCode)
|
||||||
|
// Save the token in the interceptor.
|
||||||
|
.doOnNext { interceptor.setAuth(it) }
|
||||||
|
// Obtain the authenticated user from the API.
|
||||||
|
.zipWith(api.getCurrentUser().map { it["id"].toString() })
|
||||||
|
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
|
||||||
|
// Save service credentials (username and refresh token).
|
||||||
|
.doOnNext { saveCredentials(it.first, it.second) }
|
||||||
|
// Logout on any error.
|
||||||
|
.doOnError { logout() }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
interceptor.setAuth(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String): Observable<List<MangaSync>> {
|
||||||
|
return api.search(query, 1)
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
|
.filter { it.type != "Novel" }
|
||||||
|
.map { it.toMangaSync() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(): Observable<List<MangaSync>> {
|
||||||
|
return api.getList(getUsername())
|
||||||
|
.flatMap { Observable.from(it.flatten()) }
|
||||||
|
.map { it.toMangaSync() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||||
|
manga.score.toInt())
|
||||||
|
.doOnNext { it.body().close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||||
|
.doOnError { Timber.e(it, it.message) }
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||||
|
manga.status = COMPLETED
|
||||||
|
}
|
||||||
|
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||||
|
manga.score.toInt())
|
||||||
|
.doOnNext { it.body().close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||||
|
.doOnError { Timber.e(it, it.message) }
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return getList()
|
||||||
|
.flatMap { userlist ->
|
||||||
|
manga.sync_id = id
|
||||||
|
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||||
|
if (mangaFromList != null) {
|
||||||
|
manga.copyPersonalFrom(mangaFromList)
|
||||||
|
update(manga)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
manga.score = DEFAULT_SCORE.toFloat()
|
||||||
|
manga.status = DEFAULT_STATUS
|
||||||
|
add(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MangaSync.getAnilistStatus() = when (status) {
|
||||||
|
READING -> "reading"
|
||||||
|
COMPLETED -> "completed"
|
||||||
|
ON_HOLD -> "on-hold"
|
||||||
|
DROPPED -> "dropped"
|
||||||
|
PLAN_TO_READ -> "plan to read"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.*
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
interface AnilistApi {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "tachiyomi-hrtje"
|
||||||
|
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||||
|
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||||
|
private const val baseUrl = "https://anilist.co/api/"
|
||||||
|
|
||||||
|
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||||
|
.appendQueryParameter("grant_type", "authorization_code")
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("redirect_uri", clientUrl)
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(AnilistApi::class.java)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("auth/access_token")
|
||||||
|
fun requestAccessToken(
|
||||||
|
@Field("code") code: String,
|
||||||
|
@Field("grant_type") grant_type: String = "authorization_code",
|
||||||
|
@Field("client_id") client_id: String = clientId,
|
||||||
|
@Field("client_secret") client_secret: String = clientSecret,
|
||||||
|
@Field("redirect_uri") redirect_uri: String = clientUrl)
|
||||||
|
: Observable<OAuth>
|
||||||
|
|
||||||
|
@GET("user")
|
||||||
|
fun getCurrentUser(): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("manga/search/{query}")
|
||||||
|
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
|
||||||
|
|
||||||
|
@GET("user/{username}/mangalist")
|
||||||
|
fun getList(@Path("username") username: String): Observable<ALUserLists>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("mangalist")
|
||||||
|
fun addManga(
|
||||||
|
@Field("id") id: Int,
|
||||||
|
@Field("chapters_read") chapters_read: Int,
|
||||||
|
@Field("list_status") list_status: String,
|
||||||
|
@Field("score_raw") score_raw: Int)
|
||||||
|
: Observable<Response<ResponseBody>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("mangalist")
|
||||||
|
fun updateManga(
|
||||||
|
@Field("id") id: Int,
|
||||||
|
@Field("chapters_read") chapters_read: Int,
|
||||||
|
@Field("list_status") list_status: String,
|
||||||
|
@Field("score_raw") score_raw: Int)
|
||||||
|
: Observable<Response<ResponseBody>>
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*
|
||||||
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
|
* before its original expiration date.
|
||||||
|
*/
|
||||||
|
private var oauth: OAuth? = null
|
||||||
|
set(value) {
|
||||||
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
if (refreshToken.isNullOrEmpty()) {
|
||||||
|
throw Exception("Not authenticated with Anilist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh access token if null or expired.
|
||||||
|
if (oauth == null || oauth!!.isExpired()) {
|
||||||
|
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||||
|
oauth = if (response.isSuccessful) {
|
||||||
|
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw on null auth.
|
||||||
|
if (oauth == null) {
|
||||||
|
throw Exception("Access token wasn't refreshed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
|
* and the oauth object.
|
||||||
|
*/
|
||||||
|
fun setAuth(oauth: OAuth?) {
|
||||||
|
refreshToken = oauth?.refresh_token
|
||||||
|
this.oauth = oauth
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||||
|
|
||||||
|
data class ALManga(
|
||||||
|
val id: Int,
|
||||||
|
val title_romaji: String,
|
||||||
|
val type: String,
|
||||||
|
val total_chapters: Int) {
|
||||||
|
|
||||||
|
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||||
|
remote_id = this@ALManga.id
|
||||||
|
title = title_romaji
|
||||||
|
total_chapters = this@ALManga.total_chapters
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||||
|
|
||||||
|
fun flatten() = lists.values.flatten()
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||||
|
|
||||||
|
data class ALUserManga(
|
||||||
|
val id: Int,
|
||||||
|
val list_status: String,
|
||||||
|
val score_raw: Int,
|
||||||
|
val chapters_read: Int,
|
||||||
|
val manga: ALManga) {
|
||||||
|
|
||||||
|
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||||
|
remote_id = manga.id
|
||||||
|
status = getMangaSyncStatus()
|
||||||
|
score = score_raw.toFloat()
|
||||||
|
last_chapter_read = chapters_read
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMangaSyncStatus() = when (list_status) {
|
||||||
|
"reading" -> Anilist.READING
|
||||||
|
"completed" -> Anilist.COMPLETED
|
||||||
|
"on-hold" -> Anilist.ON_HOLD
|
||||||
|
"dropped" -> Anilist.DROPPED
|
||||||
|
"plan to read" -> Anilist.PLAN_TO_READ
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val expires: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?) {
|
||||||
|
|
||||||
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.base
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
|
||||||
|
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
|
||||||
@Inject lateinit var networkService: NetworkHelper
|
|
||||||
|
|
||||||
init {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name of the manga sync service to display
|
|
||||||
abstract val name: String
|
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Observable<Boolean>
|
|
||||||
|
|
||||||
open val isLogged: Boolean
|
|
||||||
get() = !preferences.getMangaSyncUsername(this).isEmpty() &&
|
|
||||||
!preferences.getMangaSyncPassword(this).isEmpty()
|
|
||||||
|
|
||||||
abstract fun update(manga: MangaSync): Observable<Response>
|
|
||||||
|
|
||||||
abstract fun add(manga: MangaSync): Observable<Response>
|
|
||||||
|
|
||||||
abstract fun bind(manga: MangaSync): Observable<Response>
|
|
||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,222 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.myanimelist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Xml
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||||
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import eu.kanade.tachiyomi.data.network.asObservable
|
||||||
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
|
import okhttp3.Credentials
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.xmlpull.v1.XmlSerializer
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import java.io.StringWriter
|
||||||
|
|
||||||
|
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||||
|
|
||||||
|
private lateinit var headers: Headers
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val BASE_URL = "https://myanimelist.net"
|
||||||
|
|
||||||
|
private val ENTRY_TAG = "entry"
|
||||||
|
private val CHAPTER_TAG = "chapter"
|
||||||
|
private val SCORE_TAG = "score"
|
||||||
|
private val STATUS_TAG = "status"
|
||||||
|
|
||||||
|
val READING = 1
|
||||||
|
val COMPLETED = 2
|
||||||
|
val ON_HOLD = 3
|
||||||
|
val DROPPED = 4
|
||||||
|
val PLAN_TO_READ = 6
|
||||||
|
|
||||||
|
val DEFAULT_STATUS = READING
|
||||||
|
val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val username = getUsername()
|
||||||
|
val password = getPassword()
|
||||||
|
|
||||||
|
if (!username.isEmpty() && !password.isEmpty()) {
|
||||||
|
createHeaders(username, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = "MyAnimeList"
|
||||||
|
|
||||||
|
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/manga/search.xml")
|
||||||
|
.appendQueryParameter("q", query)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendPath("malappinfo.php")
|
||||||
|
.appendQueryParameter("u", username)
|
||||||
|
.appendQueryParameter("status", "all")
|
||||||
|
.appendQueryParameter("type", "manga")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/mangalist/update")
|
||||||
|
.appendPath("${manga.remote_id}.xml")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/mangalist/add")
|
||||||
|
.appendPath("${manga.remote_id}.xml")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
override fun login(username: String, password: String): Completable {
|
||||||
|
createHeaders(username, password)
|
||||||
|
return client.newCall(GET(getLoginUrl(), headers))
|
||||||
|
.asObservable()
|
||||||
|
.doOnNext { it.close() }
|
||||||
|
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String): Observable<List<MangaSync>> {
|
||||||
|
return client.newCall(GET(getSearchUrl(query), headers))
|
||||||
|
.asObservable()
|
||||||
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
|
.filter { it.select("type").text() != "Novel" }
|
||||||
|
.map {
|
||||||
|
MangaSync.create(id).apply {
|
||||||
|
title = it.selectText("title")!!
|
||||||
|
remote_id = it.selectInt("id")
|
||||||
|
total_chapters = it.selectInt("chapters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAL doesn't support score with decimals
|
||||||
|
fun getList(): Observable<List<MangaSync>> {
|
||||||
|
return networkService.forceCacheClient
|
||||||
|
.newCall(GET(getListUrl(getUsername()), headers))
|
||||||
|
.asObservable()
|
||||||
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
|
.map {
|
||||||
|
MangaSync.create(id).apply {
|
||||||
|
title = it.selectText("series_title")!!
|
||||||
|
remote_id = it.selectInt("series_mangadb_id")
|
||||||
|
last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
|
status = it.selectInt("my_status")
|
||||||
|
score = it.selectInt("my_score").toFloat()
|
||||||
|
total_chapters = it.selectInt("series_chapters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return Observable.defer {
|
||||||
|
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||||
|
manga.status = COMPLETED
|
||||||
|
}
|
||||||
|
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
||||||
|
.asObservable()
|
||||||
|
.doOnNext { it.close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return Observable.defer {
|
||||||
|
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
||||||
|
.asObservable()
|
||||||
|
.doOnNext { it.close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
||||||
|
val xml = Xml.newSerializer()
|
||||||
|
val writer = StringWriter()
|
||||||
|
|
||||||
|
with(xml) {
|
||||||
|
setOutput(writer)
|
||||||
|
startDocument("UTF-8", false)
|
||||||
|
startTag("", ENTRY_TAG)
|
||||||
|
|
||||||
|
// Last chapter read
|
||||||
|
if (manga.last_chapter_read != 0) {
|
||||||
|
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
||||||
|
}
|
||||||
|
// Manga status in the list
|
||||||
|
inTag(STATUS_TAG, manga.status.toString())
|
||||||
|
|
||||||
|
// Manga score
|
||||||
|
inTag(SCORE_TAG, manga.score.toString())
|
||||||
|
|
||||||
|
endTag("", ENTRY_TAG)
|
||||||
|
endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
val form = FormBody.Builder()
|
||||||
|
form.add("data", writer.toString())
|
||||||
|
return form.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
||||||
|
startTag(namespace, tag)
|
||||||
|
text(body)
|
||||||
|
endTag(namespace, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return getList()
|
||||||
|
.flatMap { userlist ->
|
||||||
|
manga.sync_id = id
|
||||||
|
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||||
|
if (mangaFromList != null) {
|
||||||
|
manga.copyPersonalFrom(mangaFromList)
|
||||||
|
update(manga)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
manga.score = DEFAULT_SCORE.toFloat()
|
||||||
|
manga.status = DEFAULT_STATUS
|
||||||
|
add(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHeaders(username: String, password: String) {
|
||||||
|
val builder = Headers.Builder()
|
||||||
|
builder.add("Authorization", Credentials.basic(username, password))
|
||||||
|
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||||
|
headers = builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,216 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.services
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Xml
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.network.get
|
|
||||||
import eu.kanade.tachiyomi.data.network.post
|
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
|
||||||
import okhttp3.*
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.xmlpull.v1.XmlSerializer
|
|
||||||
import rx.Observable
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
|
||||||
startTag(namespace, tag)
|
|
||||||
text(body)
|
|
||||||
endTag(namespace, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
|
||||||
|
|
||||||
private lateinit var headers: Headers
|
|
||||||
private lateinit var username: String
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val BASE_URL = "http://myanimelist.net"
|
|
||||||
|
|
||||||
private val ENTRY_TAG = "entry"
|
|
||||||
private val CHAPTER_TAG = "chapter"
|
|
||||||
private val SCORE_TAG = "score"
|
|
||||||
private val STATUS_TAG = "status"
|
|
||||||
|
|
||||||
val READING = 1
|
|
||||||
val COMPLETED = 2
|
|
||||||
val ON_HOLD = 3
|
|
||||||
val DROPPED = 4
|
|
||||||
val PLAN_TO_READ = 6
|
|
||||||
|
|
||||||
val DEFAULT_STATUS = READING
|
|
||||||
val DEFAULT_SCORE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val username = preferences.getMangaSyncUsername(this)
|
|
||||||
val password = preferences.getMangaSyncPassword(this)
|
|
||||||
|
|
||||||
if (!username.isEmpty() && !password.isEmpty()) {
|
|
||||||
createHeaders(username, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = "MyAnimeList"
|
|
||||||
|
|
||||||
fun getLoginUrl(): String {
|
|
||||||
return Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String): Observable<Boolean> {
|
|
||||||
createHeaders(username, password)
|
|
||||||
return networkService.request(get(getLoginUrl(), headers))
|
|
||||||
.map { it.code() == 200 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSearchUrl(query: String): String {
|
|
||||||
return Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/manga/search.xml")
|
|
||||||
.appendQueryParameter("q", query)
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<MangaSync>> {
|
|
||||||
return networkService.requestBody(get(getSearchUrl(query), headers))
|
|
||||||
.map { Jsoup.parse(it) }
|
|
||||||
.flatMap { Observable.from(it.select("entry")) }
|
|
||||||
.filter { it.select("type").text() != "Novel" }
|
|
||||||
.map {
|
|
||||||
val manga = MangaSync.create(this)
|
|
||||||
manga.title = it.selectText("title")
|
|
||||||
manga.remote_id = it.selectInt("id")
|
|
||||||
manga.total_chapters = it.selectInt("chapters")
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getListUrl(username: String): String {
|
|
||||||
return Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendPath("malappinfo.php")
|
|
||||||
.appendQueryParameter("u", username)
|
|
||||||
.appendQueryParameter("status", "all")
|
|
||||||
.appendQueryParameter("type", "manga")
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAL doesn't support score with decimals
|
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
|
||||||
return networkService.requestBody(get(getListUrl(username), headers), true)
|
|
||||||
.map { Jsoup.parse(it) }
|
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
|
||||||
.map {
|
|
||||||
val manga = MangaSync.create(this)
|
|
||||||
manga.title = it.selectText("series_title")
|
|
||||||
manga.remote_id = it.selectInt("series_mangadb_id")
|
|
||||||
manga.last_chapter_read = it.selectInt("my_read_chapters")
|
|
||||||
manga.status = it.selectInt("my_status")
|
|
||||||
manga.score = it.selectInt("my_score").toFloat()
|
|
||||||
manga.total_chapters = it.selectInt("series_chapters")
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUpdateUrl(manga: MangaSync): String {
|
|
||||||
return Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/update")
|
|
||||||
.appendPath(manga.remote_id.toString() + ".xml")
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(manga: MangaSync): Observable<Response> {
|
|
||||||
return Observable.defer {
|
|
||||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
|
||||||
manga.status = COMPLETED
|
|
||||||
}
|
|
||||||
networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAddUrl(manga: MangaSync): String {
|
|
||||||
return Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/add")
|
|
||||||
.appendPath(manga.remote_id.toString() + ".xml")
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(manga: MangaSync): Observable<Response> {
|
|
||||||
return Observable.defer {
|
|
||||||
networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
|
||||||
val xml = Xml.newSerializer()
|
|
||||||
val writer = StringWriter()
|
|
||||||
|
|
||||||
with(xml) {
|
|
||||||
setOutput(writer)
|
|
||||||
startDocument("UTF-8", false)
|
|
||||||
startTag("", ENTRY_TAG)
|
|
||||||
|
|
||||||
// Last chapter read
|
|
||||||
if (manga.last_chapter_read != 0) {
|
|
||||||
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
|
||||||
}
|
|
||||||
// Manga status in the list
|
|
||||||
inTag(STATUS_TAG, manga.status.toString())
|
|
||||||
|
|
||||||
// Manga score
|
|
||||||
inTag(SCORE_TAG, manga.score.toString())
|
|
||||||
|
|
||||||
endTag("", ENTRY_TAG)
|
|
||||||
endDocument()
|
|
||||||
}
|
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
|
||||||
form.add("data", writer.toString())
|
|
||||||
return form.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(manga: MangaSync): Observable<Response> {
|
|
||||||
return getList()
|
|
||||||
.flatMap {
|
|
||||||
manga.sync_id = id
|
|
||||||
for (remoteManga in it) {
|
|
||||||
if (remoteManga.remote_id == manga.remote_id) {
|
|
||||||
// Manga is already in the list
|
|
||||||
manga.copyPersonalFrom(remoteManga)
|
|
||||||
return@flatMap update(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
manga.score = DEFAULT_SCORE.toFloat()
|
|
||||||
manga.status = DEFAULT_STATUS
|
|
||||||
return@flatMap add(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
|
||||||
when (status) {
|
|
||||||
READING -> getString(R.string.reading)
|
|
||||||
COMPLETED -> getString(R.string.completed)
|
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
|
||||||
DROPPED -> getString(R.string.dropped)
|
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createHeaders(username: String, password: String) {
|
|
||||||
this.username = username
|
|
||||||
val builder = Headers.Builder()
|
|
||||||
builder.add("Authorization", Credentials.basic(username, password))
|
|
||||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
|
||||||
headers = builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,80 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
|
import com.squareup.duktape.Duktape
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
|
||||||
|
|
||||||
|
//language=RegExp
|
||||||
|
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||||
|
|
||||||
|
//language=RegExp
|
||||||
|
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||||
|
|
||||||
|
//language=RegExp
|
||||||
|
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
|
// Check if we already solved a challenge
|
||||||
|
if (response.code() != 503 &&
|
||||||
|
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Cloudflare anti-bot is on
|
||||||
|
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
|
||||||
|
&& response.header("Server", "") == "cloudflare-nginx") {
|
||||||
|
return chain.proceed(resolveChallenge(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveChallenge(response: Response): Request {
|
||||||
|
val duktape = Duktape.create()
|
||||||
|
try {
|
||||||
|
val originalRequest = response.request()
|
||||||
|
val domain = originalRequest.url().host()
|
||||||
|
val content = response.body().string()
|
||||||
|
|
||||||
|
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||||
|
Thread.sleep(4000)
|
||||||
|
|
||||||
|
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
||||||
|
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
||||||
|
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
||||||
|
|
||||||
|
if (operation == null || challenge == null || pass == null) {
|
||||||
|
throw RuntimeException("Failed resolving Cloudflare challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
val js = operation
|
||||||
|
//language=RegExp
|
||||||
|
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||||
|
//language=RegExp
|
||||||
|
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||||
|
.replace("\n", "")
|
||||||
|
|
||||||
|
val result = (duktape.evaluate(js) as Double).toInt()
|
||||||
|
|
||||||
|
val answer = "${result + domain.length}"
|
||||||
|
|
||||||
|
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
|
||||||
|
.addQueryParameter("jschl_vc", challenge)
|
||||||
|
.addQueryParameter("pass", pass)
|
||||||
|
.addQueryParameter("jschl_answer", answer)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
val referer = originalRequest.url().toString()
|
||||||
|
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
|
||||||
|
} finally {
|
||||||
|
duktape.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,78 +1,38 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.*
|
import okhttp3.Cache
|
||||||
import rx.Observable
|
import okhttp3.OkHttpClient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.CookieManager
|
|
||||||
import java.net.CookiePolicy
|
|
||||||
import java.net.CookieStore
|
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
private val client: OkHttpClient
|
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||||
private val forceCacheClient: OkHttpClient
|
|
||||||
|
|
||||||
private val cookieManager: CookieManager
|
|
||||||
|
|
||||||
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
|
||||||
originalResponse.newBuilder()
|
|
||||||
.removeHeader("Pragma")
|
|
||||||
.header("Cache-Control", "max-age=" + 600)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
private val cacheDir = "network_cache"
|
|
||||||
|
|
||||||
init {
|
private val cookieManager = PersistentCookieJar(context)
|
||||||
val cacheDir = File(context.cacheDir, cacheDir)
|
|
||||||
|
|
||||||
cookieManager = CookieManager()
|
val client = OkHttpClient.Builder()
|
||||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
.cookieJar(cookieManager)
|
||||||
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
|
.build()
|
||||||
|
|
||||||
client = OkHttpClient.Builder()
|
val forceCacheClient = client.newBuilder()
|
||||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
.addNetworkInterceptor { chain ->
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
val originalResponse = chain.proceed(chain.request())
|
||||||
.build()
|
originalResponse.newBuilder()
|
||||||
|
.removeHeader("Pragma")
|
||||||
|
.header("Cache-Control", "max-age=600")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
forceCacheClient = client.newBuilder()
|
val cloudflareClient = client.newBuilder()
|
||||||
.addNetworkInterceptor(forceCacheInterceptor)
|
.addInterceptor(CloudflareInterceptor(cookies))
|
||||||
.build()
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
val cookies: PersistentCookieStore
|
||||||
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
|
get() = cookieManager.store
|
||||||
return Observable.fromCallable {
|
|
||||||
val c = if (forceCache) forceCacheClient else client
|
|
||||||
c.newCall(request).execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
|
|
||||||
return request(request, forceCache)
|
|
||||||
.map { it.body().string() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
|
|
||||||
return Observable.fromCallable {
|
|
||||||
val progressClient = client.newBuilder()
|
|
||||||
.cache(null)
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
|
||||||
originalResponse.newBuilder()
|
|
||||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
|
|
||||||
progressClient.newCall(request).execute()
|
|
||||||
}.retry(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val cookies: CookieStore
|
|
||||||
get() = cookieManager.cookieStore
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Producer
|
||||||
|
import rx.Subscription
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
fun Call.asObservable(): Observable<Response> {
|
||||||
|
return Observable.create { subscriber ->
|
||||||
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
|
val call = if (!isExecuted) this else {
|
||||||
|
// TODO use clone method in OkHttp 3.5
|
||||||
|
val field = javaClass.getDeclaredField("client").apply { isAccessible = true }
|
||||||
|
val client = field.get(this) as OkHttpClient
|
||||||
|
client.newCall(request())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||||
|
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
||||||
|
override fun request(n: Long) {
|
||||||
|
if (n == 0L || !compareAndSet(false, true)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = call.execute()
|
||||||
|
if (!subscriber.isUnsubscribed) {
|
||||||
|
subscriber.onNext(response)
|
||||||
|
subscriber.onCompleted()
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
if (!subscriber.isUnsubscribed) {
|
||||||
|
subscriber.onError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unsubscribe() {
|
||||||
|
call.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isUnsubscribed(): Boolean {
|
||||||
|
return call.isCanceled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber.add(requestArbiter)
|
||||||
|
subscriber.setProducer(requestArbiter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
|
val progressClient = newBuilder()
|
||||||
|
.cache(null)
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse.newBuilder()
|
||||||
|
.body(ProgressResponseBody(originalResponse.body(), listener))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return progressClient.newCall(request)
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
class PersistentCookieJar(context: Context) : CookieJar {
|
||||||
|
|
||||||
|
val store = PersistentCookieStore(context)
|
||||||
|
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
store.addAll(url, cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
return store.get(url)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class PersistentCookieStore(context: Context) {
|
||||||
|
|
||||||
|
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||||
|
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
init {
|
||||||
|
for ((key, value) in prefs.all) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val cookies = value as? Set<String>
|
||||||
|
if (cookies != null) {
|
||||||
|
try {
|
||||||
|
val url = HttpUrl.parse("http://$key")
|
||||||
|
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
||||||
|
.filter { !it.hasExpired() }
|
||||||
|
cookieMap.put(key, nonExpiredCookies)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
synchronized(this) {
|
||||||
|
val key = url.uri().host
|
||||||
|
|
||||||
|
// Append or replace the cookies for this domain.
|
||||||
|
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||||
|
for (cookie in cookies) {
|
||||||
|
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||||
|
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
||||||
|
if (pos == -1) {
|
||||||
|
cookiesForDomain.add(cookie)
|
||||||
|
} else {
|
||||||
|
cookiesForDomain[pos] = cookie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cookieMap.put(key, cookiesForDomain)
|
||||||
|
|
||||||
|
// Get cookies to be stored in disk
|
||||||
|
val newValues = cookiesForDomain.asSequence()
|
||||||
|
.filter { it.persistent() && !it.hasExpired() }
|
||||||
|
.map { it.toString() }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
prefs.edit().putStringSet(key, newValues).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAll() {
|
||||||
|
synchronized(this) {
|
||||||
|
prefs.edit().clear().apply()
|
||||||
|
cookieMap.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(url: HttpUrl) = get(url.uri().host)
|
||||||
|
|
||||||
|
fun get(uri: URI) = get(uri.host)
|
||||||
|
|
||||||
|
private fun get(url: String): List<Cookie> {
|
||||||
|
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
||||||
|
|
||||||
|
}
|
@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
|
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||||
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
||||||
|
|
||||||
@JvmOverloads
|
fun GET(url: String,
|
||||||
fun get(url: String,
|
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||||
|
|
||||||
@ -19,8 +18,7 @@ fun get(url: String,
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
fun POST(url: String,
|
||||||
fun post(url: String,
|
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
body: RequestBody = DEFAULT_BODY,
|
body: RequestBody = DEFAULT_BODY,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
@ -0,0 +1,101 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class stores the keys for the preferences in the application. Most of them are defined
|
||||||
|
* in the file "keys.xml". By using this class we can define preferences in one place and get them
|
||||||
|
* referenced here.
|
||||||
|
*/
|
||||||
|
class PreferenceKeys(context: Context) {
|
||||||
|
|
||||||
|
val theme = context.getString(R.string.pref_theme_key)
|
||||||
|
|
||||||
|
val rotation = context.getString(R.string.pref_rotation_type_key)
|
||||||
|
|
||||||
|
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
|
||||||
|
|
||||||
|
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
|
||||||
|
|
||||||
|
val fullscreen = context.getString(R.string.pref_fullscreen_key)
|
||||||
|
|
||||||
|
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
|
||||||
|
|
||||||
|
val customBrightness = context.getString(R.string.pref_custom_brightness_key)
|
||||||
|
|
||||||
|
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
||||||
|
|
||||||
|
val colorFilter = context.getString(R.string.pref_color_filter_key)
|
||||||
|
|
||||||
|
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
|
||||||
|
|
||||||
|
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
||||||
|
|
||||||
|
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
|
||||||
|
|
||||||
|
val imageDecoder = context.getString(R.string.pref_image_decoder_key)
|
||||||
|
|
||||||
|
val zoomStart = context.getString(R.string.pref_zoom_start_key)
|
||||||
|
|
||||||
|
val readerTheme = context.getString(R.string.pref_reader_theme_key)
|
||||||
|
|
||||||
|
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
|
||||||
|
|
||||||
|
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
|
||||||
|
|
||||||
|
val reencodeImage = context.getString(R.string.pref_reencode_key)
|
||||||
|
|
||||||
|
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key)
|
||||||
|
|
||||||
|
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key)
|
||||||
|
|
||||||
|
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
||||||
|
|
||||||
|
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key)
|
||||||
|
|
||||||
|
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key)
|
||||||
|
|
||||||
|
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
||||||
|
|
||||||
|
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
||||||
|
|
||||||
|
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
|
||||||
|
|
||||||
|
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
||||||
|
|
||||||
|
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
|
||||||
|
|
||||||
|
val downloadThreads = context.getString(R.string.pref_download_slots_key)
|
||||||
|
|
||||||
|
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
||||||
|
|
||||||
|
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
||||||
|
|
||||||
|
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
||||||
|
|
||||||
|
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key)
|
||||||
|
|
||||||
|
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
|
||||||
|
|
||||||
|
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
|
||||||
|
|
||||||
|
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
||||||
|
|
||||||
|
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
||||||
|
|
||||||
|
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
||||||
|
|
||||||
|
val startScreen = context.getString(R.string.pref_start_screen_key)
|
||||||
|
|
||||||
|
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
|
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
|
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
|
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user