mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 12:07:52 +02:00
Compare commits
97 Commits
Author | SHA1 | Date | |
---|---|---|---|
0db4fcc27e | |||
f3080b6277 | |||
69cbbd5811 | |||
0b85760939 | |||
03f3a4805f | |||
d95adf2631 | |||
e971d40e06 | |||
c65a01a5f0 | |||
8586014e17 | |||
bdfae4ba04 | |||
75cb94b51a | |||
2f6d163a7a | |||
ecfe72bcad | |||
e6ff9e18cc | |||
3c550c1781 | |||
537693f5cf | |||
5ae0589547 | |||
71fc6fc257 | |||
c0d7b16ee6 | |||
f3f7aa9e1d | |||
43355970db | |||
bfa386acba | |||
e8b432485d | |||
a12a34e3bb | |||
b79855c01d | |||
17fe501a6d | |||
8201b367ec | |||
6c242084ca | |||
aefe7b176a | |||
6059b85e58 | |||
aa46c52eee | |||
d3cbfbdb59 | |||
cc9b77b876 | |||
1568ac9e8a | |||
1129dacdfa | |||
fab7967018 | |||
bb40a4d6b8 | |||
90d27147e6 | |||
634247c590 | |||
fd8f7ea693 | |||
5eeb497f2b | |||
505e642691 | |||
74a7e2a17e | |||
5fec956ce6 | |||
1794782323 | |||
0210ee8747 | |||
ca412832ef | |||
1089c25b8f | |||
e85841784c | |||
ca2236958a | |||
c7686323b7 | |||
73d1a1a05e | |||
211f7b591b | |||
72ea256906 | |||
f521622d4d | |||
dc5283ce9a | |||
256a4197c9 | |||
a5a12f8b3a | |||
bbe180ecd1 | |||
67678cd49e | |||
097d4fe34c | |||
5914346ace | |||
062788f222 | |||
55be9b9ca5 | |||
0da2f91771 | |||
ff190e02d4 | |||
29fd5747eb | |||
fa8f5bc0d8 | |||
2118434823 | |||
2eeac0bf8b | |||
89b293fecd | |||
3f758d5981 | |||
e838bb43d2 | |||
b7b83305b2 | |||
8df3080e0d | |||
f88794c752 | |||
cc9e2cee1f | |||
91cb892c74 | |||
a26f908370 | |||
4d14f56fa8 | |||
d9a2255be9 | |||
5e3d71c6c5 | |||
619d94bf36 | |||
6069659e0f | |||
f6a79bde6f | |||
bb9e230b35 | |||
bc9417e16b | |||
a4313d388d | |||
4ebb3a894d | |||
0642889b64 | |||
3094d084d6 | |||
f9fec74ffd | |||
8ef3ab0d49 | |||
e9a6f8ef46 | |||
68724752f8 | |||
de8fa09366 | |||
e619870eec |
30
.github/CONTRIBUTING.md
vendored
30
.github/CONTRIBUTING.md
vendored
@ -1,6 +1,17 @@
|
|||||||
|
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
|
2. If you are unsure, ask here: [](https://discord.gg/WrBkRk4)
|
||||||
|
3. What is your type of issue?
|
||||||
|
* [Catalogue request](#catalogue-requests)
|
||||||
|
* [Bugs](#bugs)
|
||||||
|
* [Feature requests](#feature-requests)
|
||||||
|
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
||||||
|
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
# Catalogue requests
|
# Catalogue requests
|
||||||
|
|
||||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here
|
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||||
|
|
||||||
# Bugs
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (Setting > About > Version)
|
||||||
@ -8,17 +19,9 @@
|
|||||||
* Dev version is equal to the number of commits as seen in the main page
|
* Dev version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* For multipart issues **use list** like this:
|
* Don't group unrelated requests into one issue
|
||||||
* [x] Done
|
|
||||||
* [ ] Not done
|
|
||||||
```
|
|
||||||
* [x] Done
|
|
||||||
* [ ] Not done
|
|
||||||
```
|
|
||||||
* Don't put together too many unrelated requests into one issue
|
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
@ -28,8 +31,3 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|||||||
|
|
||||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
|
|
||||||
# Translations
|
|
||||||
|
|
||||||
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
|
||||||
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
|
||||||
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@ -1,7 +1 @@
|
|||||||
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
||||||
|
|
||||||
Remove line above and describe your issue here. Fill out version below. Use Preview.
|
|
||||||
|
|
||||||
|
|
||||||
Version: r000 or v0.0.0
|
|
||||||
(other relevant info like OS)
|
|
||||||
|
11
README.md
11
README.md
@ -1,10 +1,11 @@
|
|||||||
| Build | Download | F-Droid |
|
| Build | Download | F-Droid | Contribute | Contact |
|
||||||
|-------|----------|-------------|
|
|-------|----------|---------|------------|---------|
|
||||||
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) |
|
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [](https://github.com/inorichi/tachiyomi/wiki/Translation) | [](https://discord.gg/WrBkRk4) |
|
||||||
|
|
||||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
### **Contact us on [Discord](https://discord.gg/WrBkRk4)**
|
||||||
|
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
|
||||||
|
|
||||||
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
|
***
|
||||||
|
|
||||||
Tachiyomi is a free and open source manga reader for Android.
|
Tachiyomi is a free and open source manga reader for Android.
|
||||||
|
|
||||||
|
100
app/build.gradle
100
app/build.gradle
@ -38,8 +38,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 21
|
versionCode 26
|
||||||
versionName "0.5.1"
|
versionName "0.6.3"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -92,20 +92,17 @@ android {
|
|||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||||
|
compile 'com.github.inorichi:tachimage:68cd311'
|
||||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '25.2.0'
|
final support_library_version = '25.4.0'
|
||||||
compile "com.android.support:support-v4:$support_library_version"
|
compile "com.android.support:support-v4:$support_library_version"
|
||||||
compile "com.android.support:appcompat-v7:$support_library_version"
|
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||||
compile "com.android.support:cardview-v7:$support_library_version"
|
compile "com.android.support:cardview-v7:$support_library_version"
|
||||||
@ -120,23 +117,23 @@ dependencies {
|
|||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.2.7'
|
compile 'io.reactivex:rxjava:1.3.0'
|
||||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.6.0"
|
compile "com.squareup.okhttp3:okhttp:3.8.1"
|
||||||
compile 'com.squareup.okio:okio:1.11.0'
|
compile 'com.squareup.okio:okio:1.13.0'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.2.0'
|
final retrofit_version = '2.3.0'
|
||||||
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.8.0'
|
compile 'com.google.code.gson:gson:2.8.1'
|
||||||
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
// YAML
|
// YAML
|
||||||
@ -153,29 +150,28 @@ dependencies {
|
|||||||
compile 'org.jsoup:jsoup:1.10.2'
|
compile 'org.jsoup:jsoup:1.10.2'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
compile 'com.evernote:android-job:1.1.7'
|
compile 'com.evernote:android-job:1.1.11'
|
||||||
compile 'com.google.android.gms:play-services-gcm:10.2.0'
|
compile 'com.google.android.gms:play-services-gcm:11.0.1'
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:1.12.3"
|
compile "com.pushtorefresh.storio:sqlite:1.13.0"
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
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"
|
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
compile 'com.github.bumptech.glide:glide:3.8.0'
|
||||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
|
||||||
// Transformations
|
// Transformations
|
||||||
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||||
@ -190,13 +186,22 @@ dependencies {
|
|||||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
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:5.0.0-rc1'
|
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
||||||
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
|
|
||||||
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.afollestad.material-dialogs:core:0.9.4.1'
|
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
|
||||||
compile 'net.xpece.android:support-preference:1.2.5'
|
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||||
|
|
||||||
|
// Conductor
|
||||||
|
compile "com.bluelinelabs:conductor:2.1.4"
|
||||||
|
compile 'com.github.inorichi:conductor-support-preference:9e36460'
|
||||||
|
|
||||||
|
// RxBindings
|
||||||
|
final rxbindings_version = '1.0.1'
|
||||||
|
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||||
|
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||||
|
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||||
|
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
@ -212,7 +217,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.1.1'
|
ext.kotlin_version = '1.1.4'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -224,3 +229,50 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround to force a support lib version
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy.eachDependency { details ->
|
||||||
|
def requested = details.requested
|
||||||
|
if (requested.group == 'com.android.support') {
|
||||||
|
if (!requested.name.startsWith("multidex")) {
|
||||||
|
details.useVersion '25.4.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add support for placeholders in resource files
|
||||||
|
//https://code.google.com/p/android/issues/detail?id=69224
|
||||||
|
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
|
||||||
|
def file = new File(basePath, fileName);
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Replacing placeholders in " + file.toString())
|
||||||
|
logger.debug("Placeholders: " + placeholders.toString())
|
||||||
|
|
||||||
|
def content = file.getText('UTF-8')
|
||||||
|
|
||||||
|
placeholders.each { entry ->
|
||||||
|
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(content, 'UTF-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
output.processResources.doFirst {
|
||||||
|
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
|
||||||
|
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
|
||||||
|
|
||||||
|
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
app/proguard-rules.pro
vendored
52
app/proguard-rules.pro
vendored
@ -1,23 +1,24 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
|
-dontwarn eu.kanade.tachiyomi.**
|
||||||
-keep class eu.kanade.tachiyomi.**
|
-keep class eu.kanade.tachiyomi.**
|
||||||
|
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
||||||
|
|
||||||
-keep class com.hippo.image.** { *; }
|
-keep class com.hippo.image.** { *; }
|
||||||
-keep interface com.hippo.image.** { *; }
|
-keep interface com.hippo.image.** { *; }
|
||||||
|
|
||||||
# OkHttp
|
# Extensions may require methods unused in the core app
|
||||||
-keepattributes Signature
|
-keep class org.jsoup.** { *; }
|
||||||
-keepattributes *Annotation*
|
-keep class kotlin.** { *; }
|
||||||
-keep class okhttp3.** { *; }
|
-keep class okhttp3.** { *; }
|
||||||
-keep interface okhttp3.** { *; }
|
-keep class com.google.gson.** { *; }
|
||||||
|
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||||
|
|
||||||
|
# OkHttp
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
# Okio
|
-dontwarn retrofit2.Platform$Java8
|
||||||
-keep class sun.misc.Unsafe { *; }
|
|
||||||
-dontwarn java.nio.file.*
|
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
|
||||||
-dontwarn okio.**
|
|
||||||
|
|
||||||
# Glide specific rules #
|
# Glide specific rules #
|
||||||
# https://github.com/bumptech/glide
|
# https://github.com/bumptech/glide
|
||||||
@ -43,27 +44,26 @@
|
|||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Retrofit 2.X
|
### Support v7, Design
|
||||||
## https://square.github.io/retrofit/ ##
|
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
||||||
|
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
||||||
|
|
||||||
-dontwarn retrofit2.**
|
|
||||||
-keep class retrofit2.** { *; }
|
|
||||||
-keepattributes Signature
|
|
||||||
-keepattributes Exceptions
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@retrofit2.http.* <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# AppCombat
|
|
||||||
-keep public class android.support.v7.widget.** { *; }
|
-keep public class android.support.v7.widget.** { *; }
|
||||||
-keep public class android.support.v7.internal.widget.** { *; }
|
-keep public class android.support.v7.internal.widget.** { *; }
|
||||||
-keep public class android.support.v7.internal.view.menu.** { *; }
|
-keep public class android.support.v7.internal.view.menu.** { *; }
|
||||||
|
-keep public class android.support.v7.graphics.drawable.** { *; }
|
||||||
|
|
||||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||||
public <init>(android.content.Context);
|
public <init>(android.content.Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-dontwarn android.support.**
|
||||||
|
-dontwarn android.support.design.**
|
||||||
|
-keep class android.support.design.** { *; }
|
||||||
|
-keep interface android.support.design.** { *; }
|
||||||
|
-keep public class android.support.design.R$* { *; }
|
||||||
|
|
||||||
|
|
||||||
# ReactiveNetwork
|
# ReactiveNetwork
|
||||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||||
|
|
||||||
@ -73,15 +73,8 @@
|
|||||||
# removes such information by default, so configure it to keep all of it.
|
# removes such information by default, so configure it to keep all of it.
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# Gson specific classes
|
# Gson specific classes
|
||||||
-keep class sun.misc.Unsafe { *; }
|
-keep class sun.misc.Unsafe { *; }
|
||||||
#-keep class com.google.gson.stream.** { *; }
|
|
||||||
|
|
||||||
# Application classes that will be serialized/deserialized over Gson
|
|
||||||
-keep class com.google.gson.examples.android.model.** { *; }
|
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
@ -91,7 +84,6 @@
|
|||||||
|
|
||||||
# SnakeYaml
|
# SnakeYaml
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
|
||||||
-dontwarn org.yaml.snakeyaml.**
|
-dontwarn org.yaml.snakeyaml.**
|
||||||
|
|
||||||
# Duktape
|
# Duktape
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@ -9,9 +8,6 @@
|
|||||||
<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" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@ -19,33 +15,26 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
<activity android:name=".ui.main.MainActivity">
|
<activity
|
||||||
|
android:name=".ui.main.MainActivity"
|
||||||
|
android:launchMode="singleTop">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts"/>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.manga.MangaActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:theme="@style/Theme.Reader" />
|
android:theme="@style/Theme.Reader" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/label_settings"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.category.CategoryActivity"
|
|
||||||
android:label="@string/label_categories"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme" />
|
android:theme="@style/FilePickerTheme" />
|
||||||
<activity
|
<activity
|
||||||
@ -62,9 +51,6 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.download.DownloadActivity"
|
|
||||||
android:launchMode="singleTop" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
@ -102,6 +88,14 @@
|
|||||||
android:name=".data.updater.UpdateDownloaderService"
|
android:name=".data.updater.UpdateDownloaderService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupCreateService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupRestoreService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
android:value="GlideModule" />
|
android:value="GlideModule" />
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@ -5,6 +5,7 @@ import android.content.Context
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.support.multidex.MultiDex
|
import android.support.multidex.MultiDex
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
@ -58,6 +59,7 @@ open class App : Application() {
|
|||||||
when (tag) {
|
when (tag) {
|
||||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
||||||
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
UpdateCheckerJob.TAG -> UpdateCheckerJob()
|
||||||
|
BackupCreatorJob.TAG -> BackupCreatorJob()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val NOTIFICATION_LIBRARY_ID = 1
|
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
|
||||||
const val NOTIFICATION_UPDATER_ID = 2
|
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
const val NOTIFICATION_UPDATER_ID = 3
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
|
||||||
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
|
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
|
||||||
|
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
|
||||||
}
|
}
|
||||||
|
63
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
63
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object Migrations {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a migration when the application is updated.
|
||||||
|
*
|
||||||
|
* @param preferences Preferences of the application.
|
||||||
|
* @return true if a migration is performed, false otherwise.
|
||||||
|
*/
|
||||||
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
|
val context = preferences.context
|
||||||
|
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||||
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
if (oldVersion == 0) return false
|
||||||
|
|
||||||
|
if (oldVersion < 14) {
|
||||||
|
// Restore jobs after upgrading to evernote's job scheduler.
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||||
|
UpdateCheckerJob.setupTask()
|
||||||
|
}
|
||||||
|
LibraryUpdateJob.setupTask()
|
||||||
|
}
|
||||||
|
if (oldVersion < 15) {
|
||||||
|
// Delete internal chapter cache dir.
|
||||||
|
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||||
|
}
|
||||||
|
if (oldVersion < 19) {
|
||||||
|
// Move covers to external files dir.
|
||||||
|
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
||||||
|
if (oldDir.exists()) {
|
||||||
|
val destDir = context.getExternalFilesDir("covers")
|
||||||
|
if (destDir != null) {
|
||||||
|
oldDir.listFiles().forEach {
|
||||||
|
it.renameTo(File(destDir, it.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 26) {
|
||||||
|
// Delete external chapter cache dir.
|
||||||
|
val extCache = context.externalCacheDir
|
||||||
|
if (extCache != null) {
|
||||||
|
val chapterCache = File(extCache, "chapter_disk_cache")
|
||||||
|
if (chapterCache.exists()) {
|
||||||
|
chapterCache.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
|
||||||
|
object BackupConst {
|
||||||
|
|
||||||
|
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||||
|
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||||
|
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||||
|
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||||
|
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||||
|
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||||
|
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||||
|
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||||
|
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||||
|
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||||
|
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||||
|
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||||
|
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||||
|
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||||
|
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||||
|
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.IntentService
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
|
import timber.log.Timber
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [IntentService] used to backup [Manga] information to [JsonArray]
|
||||||
|
*/
|
||||||
|
class BackupCreateService : IntentService(NAME) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Name of class
|
||||||
|
private const val NAME = "BackupCreateService"
|
||||||
|
|
||||||
|
// Backup called from job
|
||||||
|
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
||||||
|
// Options for backup
|
||||||
|
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
internal const val BACKUP_CATEGORY = 0x1
|
||||||
|
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
internal const val BACKUP_CHAPTER = 0x2
|
||||||
|
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
internal const val BACKUP_HISTORY = 0x4
|
||||||
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
internal const val BACKUP_TRACK = 0x8
|
||||||
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
|
internal const val BACKUP_ALL = 0xF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a backup from library
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path path of Uri
|
||||||
|
* @param flags determines what to backup
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun makeBackup(context: Context, uri: Uri, flags: Int, isJob: Boolean = false) {
|
||||||
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
putExtra(EXTRA_IS_JOB, isJob)
|
||||||
|
putExtra(EXTRA_FLAGS, flags)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val backupManager by lazy { BackupManager(this) }
|
||||||
|
|
||||||
|
override fun onHandleIntent(intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
// Get values
|
||||||
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
|
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
||||||
|
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||||
|
// Create backup
|
||||||
|
createBackupFromApp(uri, flags, isJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
|
||||||
|
// Create root object
|
||||||
|
val root = JsonObject()
|
||||||
|
|
||||||
|
// Create information object
|
||||||
|
val information = JsonObject()
|
||||||
|
|
||||||
|
// Create manga array
|
||||||
|
val mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Add value's to root
|
||||||
|
root[VERSION] = Backup.CURRENT_VERSION
|
||||||
|
root[MANGAS] = mangaEntries
|
||||||
|
root[CATEGORIES] = categoryEntries
|
||||||
|
|
||||||
|
backupManager.databaseHelper.inTransaction {
|
||||||
|
// Get manga from database
|
||||||
|
val mangas = backupManager.getFavoriteManga()
|
||||||
|
|
||||||
|
// Backup library manga and its dependencies
|
||||||
|
mangas.forEach { manga ->
|
||||||
|
mangaEntries.add(backupManager.backupMangaObject(manga, flags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup categories
|
||||||
|
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||||
|
backupManager.backupCategories(categoryEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When BackupCreatorJob
|
||||||
|
if (isJob) {
|
||||||
|
// Get dir of file
|
||||||
|
val dir = UniFile.fromUri(this, uri)
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = backupManager.numberOfBackups()
|
||||||
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
val newFile = dir.createFile(Backup.getDefaultFilename())
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
newFile.openOutputStream().bufferedWriter().use {
|
||||||
|
backupManager.parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val file = UniFile.fromUri(this, uri)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
file.openOutputStream().bufferedWriter().use {
|
||||||
|
backupManager.parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show completed dialog
|
||||||
|
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||||
|
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
if (!isJob) {
|
||||||
|
// Show error dialog
|
||||||
|
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
|
||||||
|
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.evernote.android.job.Job
|
||||||
|
import com.evernote.android.job.JobManager
|
||||||
|
import com.evernote.android.job.JobRequest
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class BackupCreatorJob : Job() {
|
||||||
|
|
||||||
|
override fun onRunJob(params: Params): Result {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
|
||||||
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
|
BackupCreateService.makeBackup(context, uri, flags, true)
|
||||||
|
return Result.SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "BackupCreator"
|
||||||
|
|
||||||
|
fun setupTask(prefInterval: Int? = null) {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
||||||
|
if (interval > 0) {
|
||||||
|
JobRequest.Builder(TAG)
|
||||||
|
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||||
|
.setPersisted(true)
|
||||||
|
.setUpdateCurrent(true)
|
||||||
|
.build()
|
||||||
|
.schedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTask() {
|
||||||
|
JobManager.instance().cancelAllForTag(TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,203 +1,212 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import android.content.Context
|
||||||
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.google.gson.stream.JsonReader
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.BooleanSerializer
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.LongSerializer
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.*
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import java.io.*
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.util.*
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
* This class provides the necessary methods to create and restore backups for the data of the
|
|
||||||
* application. The backup follows a JSON structure, with the following scheme:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "mangas": [
|
|
||||||
* {
|
|
||||||
* "manga": {"id": 1, ...},
|
|
||||||
* "chapters": [{"id": 1, ...}, {...}],
|
|
||||||
* "sync": [{"id": 1, ...}, {...}],
|
|
||||||
* "categories": ["cat1", "cat2", ...]
|
|
||||||
* },
|
|
||||||
* { ... }
|
|
||||||
* ],
|
|
||||||
* "categories": [
|
|
||||||
* {"id": 1, ...},
|
|
||||||
* {"id": 2, ...}
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param db the database helper.
|
|
||||||
*/
|
|
||||||
class BackupManager(private val db: DatabaseHelper) {
|
|
||||||
|
|
||||||
private val MANGA = "manga"
|
|
||||||
private val MANGAS = "mangas"
|
|
||||||
private val CHAPTERS = "chapters"
|
|
||||||
private val TRACK = "sync"
|
|
||||||
private val CATEGORIES = "categories"
|
|
||||||
|
|
||||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
|
||||||
private val gson = GsonBuilder()
|
|
||||||
.registerTypeAdapter(java.lang.Integer::class.java, IntegerSerializer())
|
|
||||||
.registerTypeAdapter(java.lang.Boolean::class.java, BooleanSerializer())
|
|
||||||
.registerTypeAdapter(java.lang.Long::class.java, LongSerializer())
|
|
||||||
.setExclusionStrategies(IdExclusion())
|
|
||||||
.create()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backups the data of the application to a file.
|
* Database.
|
||||||
*
|
|
||||||
* @param file the file where the backup will be saved.
|
|
||||||
* @throws IOException if there's any IO error.
|
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||||
fun backupToFile(file: File) {
|
|
||||||
val root = backupToJson()
|
|
||||||
|
|
||||||
FileWriter(file).use {
|
/**
|
||||||
gson.toJson(root, it)
|
* Source manager.
|
||||||
|
*/
|
||||||
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of parser
|
||||||
|
*/
|
||||||
|
var version: Int = version
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json Parser
|
||||||
|
*/
|
||||||
|
var parser: Gson = initParser()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set version of parser
|
||||||
|
*
|
||||||
|
* @param version version of parser
|
||||||
|
*/
|
||||||
|
internal fun setVersion(version: Int) {
|
||||||
|
this.version = version
|
||||||
|
parser = initParser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initParser(): Gson {
|
||||||
|
return when (version) {
|
||||||
|
1 -> GsonBuilder().create()
|
||||||
|
2 -> GsonBuilder()
|
||||||
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
|
.create()
|
||||||
|
else -> throw Exception("Json version unknown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a JSON object containing the backup of the app's data.
|
* Backup the categories of library
|
||||||
*
|
*
|
||||||
* @return the backup as a JSON object.
|
* @param root root of categories json
|
||||||
*/
|
*/
|
||||||
fun backupToJson(): JsonObject {
|
internal fun backupCategories(root: JsonArray) {
|
||||||
val root = JsonObject()
|
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
// Backup library mangas and its dependencies
|
|
||||||
val mangaEntries = JsonArray()
|
|
||||||
root.add(MANGAS, mangaEntries)
|
|
||||||
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
|
|
||||||
mangaEntries.add(backupManga(manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
val categoryEntries = JsonArray()
|
|
||||||
root.add(CATEGORIES, categoryEntries)
|
|
||||||
for (category in db.getCategories().executeAsBlocking()) {
|
|
||||||
categoryEntries.add(backupCategory(category))
|
|
||||||
}
|
|
||||||
|
|
||||||
return root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
|
* Convert a manga to Json
|
||||||
*
|
*
|
||||||
* @param manga the manga to backup.
|
* @param manga manga that gets converted
|
||||||
* @return a JSON object containing all the data of the manga.
|
* @return [JsonElement] containing manga information
|
||||||
*/
|
*/
|
||||||
private fun backupManga(manga: Manga): JsonObject {
|
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val entry = JsonObject()
|
val entry = JsonObject()
|
||||||
|
|
||||||
// Backup manga fields
|
// Backup manga fields
|
||||||
entry.add(MANGA, gson.toJsonTree(manga))
|
entry[MANGA] = parser.toJsonTree(manga)
|
||||||
|
|
||||||
// Backup all the chapters
|
// Check if user wants chapter information in backup
|
||||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
if (!chapters.isEmpty()) {
|
// Backup all the chapters
|
||||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
}
|
if (!chapters.isEmpty()) {
|
||||||
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
// Backup tracks
|
if (chaptersJson.asJsonArray.size() > 0) {
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
entry[CHAPTERS] = chaptersJson
|
||||||
if (!tracks.isEmpty()) {
|
}
|
||||||
entry.add(TRACK, gson.toJsonTree(tracks))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup categories for this manga
|
// Check if user wants category information in backup
|
||||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
if (!categoriesForManga.isEmpty()) {
|
// Backup categories for this manga
|
||||||
val categoriesNames = ArrayList<String>()
|
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
for (category in categoriesForManga) {
|
if (!categoriesForManga.isEmpty()) {
|
||||||
categoriesNames.add(category.name)
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
|
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
if (!tracks.isEmpty()) {
|
||||||
|
entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
if (!historyForManga.isEmpty()) {
|
||||||
|
val historyData = historyForManga.mapNotNull { history ->
|
||||||
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
url?.let { DHistory(url, history.last_read) }
|
||||||
|
}
|
||||||
|
val historyJson = parser.toJsonTree(historyData)
|
||||||
|
if (historyJson.asJsonArray.size() > 0) {
|
||||||
|
entry[HISTORY] = historyJson
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
* Backups a category.
|
manga.id = dbManga.id
|
||||||
*
|
manga.copyFrom(dbManga)
|
||||||
* @param category the category to backup.
|
manga.favorite = true
|
||||||
* @return a JSON object containing the data of the category.
|
insertManga(manga)
|
||||||
*/
|
|
||||||
private fun backupCategory(category: Category): JsonElement {
|
|
||||||
return gson.toJsonTree(category)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a backup from a file.
|
* [Observable] that fetches manga information
|
||||||
*
|
*
|
||||||
* @param file the file containing the backup.
|
* @param source source of manga
|
||||||
* @throws IOException if there's any IO error.
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||||
fun restoreFromFile(file: File) {
|
return source.fetchMangaDetails(manga)
|
||||||
JsonReader(FileReader(file)).use {
|
.map { networkManga ->
|
||||||
val root = JsonParser().parse(it).asJsonObject
|
manga.copyFrom(networkManga)
|
||||||
restoreFromJson(root)
|
manga.favorite = true
|
||||||
}
|
manga.initialized = true
|
||||||
|
manga.id = insertManga(manga)
|
||||||
|
manga
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a backup from an input stream.
|
* [Observable] that fetches chapter information
|
||||||
*
|
*
|
||||||
* @param stream the stream containing the backup.
|
* @param source source of manga
|
||||||
* @throws IOException if there's any IO error.
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
fun restoreFromStream(stream: InputStream) {
|
return source.fetchChapterList(manga)
|
||||||
JsonReader(InputStreamReader(stream)).use {
|
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||||
val root = JsonParser().parse(it).asJsonObject
|
.doOnNext {
|
||||||
restoreFromJson(root)
|
if (it.first.isNotEmpty()) {
|
||||||
}
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
insertChapters(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a backup from a JSON object. Everything executes in a single transaction so that
|
* Restore the categories from Json
|
||||||
* nothing is modified if there's an error.
|
|
||||||
*
|
*
|
||||||
* @param root the root of the JSON.
|
* @param jsonCategories array containing categories
|
||||||
*/
|
*/
|
||||||
fun restoreFromJson(root: JsonObject) {
|
internal fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
db.inTransaction {
|
|
||||||
// Restore categories
|
|
||||||
root.get(CATEGORIES)?.let {
|
|
||||||
restoreCategories(it.asJsonArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore mangas
|
|
||||||
root.get(MANGAS)?.let {
|
|
||||||
restoreMangas(it.asJsonArray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the categories.
|
|
||||||
*
|
|
||||||
* @param jsonCategories the categories of the json.
|
|
||||||
*/
|
|
||||||
private fun restoreCategories(jsonCategories: JsonArray) {
|
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
for (category in backupCategories) {
|
backupCategories.forEach { category ->
|
||||||
// Used to know if the category is already in the db
|
// Used to know if the category is already in the db
|
||||||
var found = false
|
var found = false
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
@ -214,102 +223,20 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
// Let the db assign the id
|
// Let the db assign the id
|
||||||
category.id = null
|
category.id = null
|
||||||
val result = db.insertCategory(category).executeAsBlocking()
|
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||||
category.id = result.insertedId()?.toInt()
|
category.id = result.insertedId()?.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores all the mangas and its related data.
|
|
||||||
*
|
|
||||||
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
|
||||||
*/
|
|
||||||
private fun restoreMangas(jsonMangas: JsonArray) {
|
|
||||||
for (backupManga in jsonMangas) {
|
|
||||||
// Map every entry to objects
|
|
||||||
val element = backupManga.asJsonObject
|
|
||||||
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
|
||||||
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
|
||||||
val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
|
|
||||||
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
|
||||||
|
|
||||||
// Restore everything related to this manga
|
|
||||||
restoreManga(manga)
|
|
||||||
restoreChaptersForManga(manga, chapters)
|
|
||||||
restoreSyncForManga(manga, tracks)
|
|
||||||
restoreCategoriesForManga(manga, categories)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to restore.
|
|
||||||
*/
|
|
||||||
private fun restoreManga(manga: Manga) {
|
|
||||||
// Try to find existing manga in db
|
|
||||||
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
|
|
||||||
if (dbManga == null) {
|
|
||||||
// Let the db assign the id
|
|
||||||
manga.id = null
|
|
||||||
val result = db.insertManga(manga).executeAsBlocking()
|
|
||||||
manga.id = result.insertedId()
|
|
||||||
} else {
|
|
||||||
// If it exists already, we copy only the values related to the source from the db
|
|
||||||
// (they can be up to date). Local values (flags) are kept from the backup.
|
|
||||||
manga.id = dbManga.id
|
|
||||||
manga.copyFrom(dbManga)
|
|
||||||
manga.favorite = true
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the chapters of a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose chapters have to be restored.
|
|
||||||
* @param chapters the chapters to restore.
|
|
||||||
*/
|
|
||||||
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
for (chapter in chapters) {
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
|
||||||
val chaptersToUpdate = ArrayList<Chapter>()
|
|
||||||
for (backupChapter in chapters) {
|
|
||||||
// Try to find existing chapter in db
|
|
||||||
val pos = dbChapters.indexOf(backupChapter)
|
|
||||||
if (pos != -1) {
|
|
||||||
// The chapter is already in the db, only update its fields
|
|
||||||
val dbChapter = dbChapters[pos]
|
|
||||||
// If one of them was read, the chapter will be marked as read
|
|
||||||
dbChapter.read = backupChapter.read || dbChapter.read
|
|
||||||
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
|
|
||||||
chaptersToUpdate.add(dbChapter)
|
|
||||||
} else {
|
|
||||||
// Insert new chapter. Let the db assign the id
|
|
||||||
backupChapter.id = null
|
|
||||||
chaptersToUpdate.add(backupChapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (!chaptersToUpdate.isEmpty()) {
|
|
||||||
db.insertChapters(chaptersToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the categories a manga is in.
|
* Restores the categories a manga is in.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose categories have to be restored.
|
* @param manga the manga whose categories have to be restored.
|
||||||
* @param categories the categories to restore.
|
* @param categories the categories to restore.
|
||||||
*/
|
*/
|
||||||
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
@ -324,45 +251,151 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
if (!mangaCategoriesToUpdate.isEmpty()) {
|
||||||
val mangaAsList = ArrayList<Manga>()
|
val mangaAsList = ArrayList<Manga>()
|
||||||
mangaAsList.add(manga)
|
mangaAsList.add(manga)
|
||||||
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||||
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val historyToBeUpdated = ArrayList<History>()
|
||||||
|
for ((url, lastRead) in history) {
|
||||||
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory.apply {
|
||||||
|
last_read = Math.max(lastRead, dbHistory.last_read)
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(dbHistory)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||||
|
val historyToAdd = History.create(it).apply {
|
||||||
|
last_read = lastRead
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(historyToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores the sync of a manga.
|
* Restores the sync of a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose sync have to be restored.
|
* @param manga the manga whose sync have to be restored.
|
||||||
* @param tracks the track list to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
for (track in tracks) {
|
tracks.map { it.manga_id = manga.id!! }
|
||||||
track.manga_id = manga.id!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
// Get tracks from database
|
||||||
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
val trackToUpdate = ArrayList<Track>()
|
val trackToUpdate = ArrayList<Track>()
|
||||||
for (backupTrack in tracks) {
|
|
||||||
// Try to find existing chapter in db
|
for (track in tracks) {
|
||||||
val pos = dbTracks.indexOf(backupTrack)
|
var isInDatabase = false
|
||||||
if (pos != -1) {
|
for (dbTrack in dbTracks) {
|
||||||
// The sync is already in the db, only update its fields
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
val dbSync = dbTracks[pos]
|
// The sync is already in the db, only update its fields
|
||||||
// Mark the max chapter as read and nothing else
|
if (track.remote_id != dbTrack.remote_id) {
|
||||||
dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read)
|
dbTrack.remote_id = track.remote_id
|
||||||
trackToUpdate.add(dbSync)
|
}
|
||||||
} else {
|
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
isInDatabase = true
|
||||||
|
trackToUpdate.add(dbTrack)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
// Insert new sync. Let the db assign the id
|
// Insert new sync. Let the db assign the id
|
||||||
backupTrack.id = null
|
track.id = null
|
||||||
trackToUpdate.add(backupTrack)
|
trackToUpdate.add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
if (!trackToUpdate.isEmpty()) {
|
if (!trackToUpdate.isEmpty()) {
|
||||||
db.insertTracks(trackToUpdate).executeAsBlocking()
|
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the chapters for manga if chapters already in database
|
||||||
|
*
|
||||||
|
* @param manga manga of chapters
|
||||||
|
* @param chapters list containing chapters that get restored
|
||||||
|
* @return boolean answering if chapter fetch is not needed
|
||||||
|
*/
|
||||||
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// Return if fetch is needed
|
||||||
|
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
|
||||||
|
return false
|
||||||
|
|
||||||
|
for (chapter in chapters) {
|
||||||
|
val pos = dbChapters.indexOf(chapter)
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
chapter.id = dbChapter.id
|
||||||
|
chapter.copyFrom(dbChapter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
chapters.filter { it.id != null }
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
insertChapters(chapters)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun getMangaFromDatabase(manga: Manga): Manga? {
|
||||||
|
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list containing manga from library
|
||||||
|
*
|
||||||
|
* @return [Manga] from library
|
||||||
|
*/
|
||||||
|
internal fun getFavoriteManga(): List<Manga> {
|
||||||
|
return databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun insertManga(manga: Manga): Long? {
|
||||||
|
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
internal fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return number of backups.
|
||||||
|
*
|
||||||
|
* @return number of backups selected by user
|
||||||
|
*/
|
||||||
|
fun numberOfBackups(): Int {
|
||||||
|
return preferences.numberOfBackups().getOrDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,406 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.chop
|
||||||
|
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores backup from json file
|
||||||
|
*/
|
||||||
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return context.isServiceRunning(BackupRestoreService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a service to restore a backup from Json
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri path of Uri
|
||||||
|
*/
|
||||||
|
fun start(context: Context, uri: Uri) {
|
||||||
|
if (!isRunning(context)) {
|
||||||
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription where the update is done.
|
||||||
|
*/
|
||||||
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The progress of a backup restore
|
||||||
|
*/
|
||||||
|
private var restoreProgress = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of manga in Json file (needed for restore)
|
||||||
|
*/
|
||||||
|
private var restoreAmount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing errors
|
||||||
|
*/
|
||||||
|
private val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup manager
|
||||||
|
*/
|
||||||
|
private lateinit var backupManager: BackupManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database
|
||||||
|
*/
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
lateinit var executor: ExecutorService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||||
|
*/
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||||
|
wakeLock.acquire()
|
||||||
|
executor = Executors.newSingleThreadExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service is destroyed. It destroys the running subscription and
|
||||||
|
* releases the wake lock.
|
||||||
|
*/
|
||||||
|
override fun onDestroy() {
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
executor.shutdown() // must be called after unsubscribe
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service receives an intent.
|
||||||
|
*
|
||||||
|
* @param intent the start intent from.
|
||||||
|
* @param flags the flags of the command.
|
||||||
|
* @param startId the start id of this command.
|
||||||
|
* @return the start value of the command.
|
||||||
|
*/
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
|
|
||||||
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
|
|
||||||
|
// Unsubscribe from any previous subscription if needed.
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
|
subscription = Observable.using(
|
||||||
|
{ db.lowLevel().beginTransaction() },
|
||||||
|
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
|
||||||
|
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||||
|
.doAfterTerminate { stopSelf(startId) }
|
||||||
|
.subscribeOn(Schedulers.from(executor))
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [Observable] containing restore process.
|
||||||
|
*
|
||||||
|
* @param uri restore file
|
||||||
|
* @return [Observable<Manga>]
|
||||||
|
*/
|
||||||
|
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||||
|
val json = JsonParser().parse(reader).asJsonObject
|
||||||
|
|
||||||
|
// Get parser version
|
||||||
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
|
backupManager = BackupManager(this, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
|
restoreProgress = 0
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
json.get(CATEGORIES)?.let {
|
||||||
|
backupManager.restoreCategories(it.asJsonArray)
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.from(mangasJson)
|
||||||
|
.concatMap {
|
||||||
|
val obj = it.asJsonObject
|
||||||
|
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
||||||
|
|
||||||
|
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
||||||
|
if (observable != null) {
|
||||||
|
observable
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
||||||
|
restoreProgress += 1
|
||||||
|
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.doOnNext {
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_TIME, time)
|
||||||
|
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||||
|
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||||
|
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
||||||
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(completeIntent)
|
||||||
|
|
||||||
|
}
|
||||||
|
.doOnError { error ->
|
||||||
|
Timber.e(error)
|
||||||
|
writeErrorLog()
|
||||||
|
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
||||||
|
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(errorIntent)
|
||||||
|
}
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write errors to error log
|
||||||
|
*/
|
||||||
|
private fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
destFile.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destFile
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga restore observable
|
||||||
|
*
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
* @return [Observable] containing manga restore information
|
||||||
|
*/
|
||||||
|
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga>? {
|
||||||
|
// Get source
|
||||||
|
val source = backupManager.sourceManager.get(manga.source) ?: return null
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
|
} else { // Manga in database
|
||||||
|
// Copy information from manga already in database
|
||||||
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
// Fetch rest of manga information
|
||||||
|
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga> {
|
||||||
|
return backupManager.restoreMangaFetchObservable(source, manga)
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap { manga ->
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(it, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(it, tracks)
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga> {
|
||||||
|
|
||||||
|
return Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(it, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(it, tracks)
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [BackupConst]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||||
|
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||||
|
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
||||||
|
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
||||||
|
putExtra(BackupConst.EXTRA_CONTENT, content)
|
||||||
|
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
||||||
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json values
|
||||||
|
*/
|
||||||
|
object Backup {
|
||||||
|
const val CURRENT_VERSION = 2
|
||||||
|
const val MANGA = "manga"
|
||||||
|
const val MANGAS = "mangas"
|
||||||
|
const val TRACK = "track"
|
||||||
|
const val CHAPTERS = "chapters"
|
||||||
|
const val CATEGORIES = "categories"
|
||||||
|
const val HISTORY = "history"
|
||||||
|
const val VERSION = "version"
|
||||||
|
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
data class DHistory(val url: String,val lastRead: Long)
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class BooleanSerializer : JsonSerializer<Boolean> {
|
|
||||||
|
|
||||||
override fun serialize(value: Boolean?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value != false)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
object CategoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<CategoryImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.name)
|
||||||
|
value(it.order)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val category = CategoryImpl()
|
||||||
|
category.name = nextString()
|
||||||
|
category.order = nextInt()
|
||||||
|
endArray()
|
||||||
|
category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
object ChapterTypeAdapter {
|
||||||
|
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<ChapterImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||||
|
beginObject()
|
||||||
|
name(URL)
|
||||||
|
value(it.url)
|
||||||
|
if (it.read) {
|
||||||
|
name(READ)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.bookmark) {
|
||||||
|
name(BOOKMARK)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.last_page_read != 0) {
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_page_read)
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val chapter = ChapterImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
val name = nextName()
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
URL -> chapter.url = nextString()
|
||||||
|
READ -> chapter.read = nextInt() == 1
|
||||||
|
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||||
|
LAST_READ -> chapter.last_page_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<DHistory> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.lastRead != 0L) {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.lastRead)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val url = nextString()
|
||||||
|
val lastRead = nextLong()
|
||||||
|
endArray()
|
||||||
|
DHistory(url, lastRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.ExclusionStrategy
|
|
||||||
import com.google.gson.FieldAttributes
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
class IdExclusion : ExclusionStrategy {
|
|
||||||
|
|
||||||
private val categoryExclusions = listOf("id")
|
|
||||||
private val mangaExclusions = listOf("id")
|
|
||||||
private val chapterExclusions = listOf("id", "manga_id")
|
|
||||||
private val syncExclusions = listOf("id", "manga_id", "update")
|
|
||||||
|
|
||||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
|
||||||
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
|
||||||
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
|
||||||
TrackImpl::class.java -> syncExclusions.contains(f.name)
|
|
||||||
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldSkipClass(clazz: Class<*>) = false
|
|
||||||
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class IntegerSerializer : JsonSerializer<Int> {
|
|
||||||
|
|
||||||
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value !== 0)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class LongSerializer : JsonSerializer<Long> {
|
|
||||||
|
|
||||||
override fun serialize(value: Long?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value !== 0L)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
object MangaTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MangaImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.title)
|
||||||
|
value(it.source)
|
||||||
|
value(it.viewer)
|
||||||
|
value(it.chapter_flags)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val manga = MangaImpl()
|
||||||
|
manga.url = nextString()
|
||||||
|
manga.title = nextString()
|
||||||
|
manga.source = nextLong()
|
||||||
|
manga.viewer = nextInt()
|
||||||
|
manga.chapter_flags = nextInt()
|
||||||
|
endArray()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val REMOTE = "r"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<TrackImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginObject()
|
||||||
|
name(TITLE)
|
||||||
|
value(it.title)
|
||||||
|
name(SYNC)
|
||||||
|
value(it.sync_id)
|
||||||
|
name(REMOTE)
|
||||||
|
value(it.remote_id)
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_chapter_read)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val track = TrackImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
val name = nextName()
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
TITLE -> track.title = nextString()
|
||||||
|
SYNC -> track.sync_id = nextInt()
|
||||||
|
REMOTE -> track.remote_id = nextInt()
|
||||||
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -45,8 +45,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
private val diskCache = DiskLruCache.open(
|
private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
|
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_VALUE_COUNT,
|
||||||
PARAMETER_CACHE_SIZE)
|
PARAMETER_CACHE_SIZE)
|
||||||
@ -82,10 +81,10 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Remove the extension from the file to get the key of the cache
|
// Remove the extension from the file to get the key of the cache
|
||||||
val key = file.substring(0, file.lastIndexOf("."))
|
val key = file.substringBeforeLast(".")
|
||||||
// Remove file from cache.
|
// Remove file from cache.
|
||||||
return diskCache.remove(key)
|
return diskCache.remove(key)
|
||||||
} catch (e: IOException) {
|
} catch (e: Exception) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,12 +186,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.
|
||||||
response.body().source().saveTo(editor.newOutputStream(0))
|
response.body()!!.source().saveTo(editor.newOutputStream(0))
|
||||||
|
|
||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
} finally {
|
} finally {
|
||||||
response.body().close()
|
response.body()?.close()
|
||||||
editor?.abortUnlessCommitted()
|
editor?.abortUnlessCommitted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,6 @@ open class DatabaseHelper(context: Context)
|
|||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|
||||||
|
fun lowLevel() = db.lowLevel()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 4
|
const val DATABASE_VERSION = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
@ -51,6 +51,9 @@ class DbOpenHelper(context: Context)
|
|||||||
if (oldVersion < 4) {
|
if (oldVersion < 4) {
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 5) {
|
||||||
|
db.execSQL(ChapterTable.addScanlator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||||
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
put(COL_URL, obj.url)
|
put(COL_URL, obj.url)
|
||||||
put(COL_NAME, obj.name)
|
put(COL_NAME, obj.name)
|
||||||
put(COL_READ, obj.read)
|
put(COL_READ, obj.read)
|
||||||
|
put(COL_SCANLATOR, obj.scanlator)
|
||||||
put(COL_BOOKMARK, obj.bookmark)
|
put(COL_BOOKMARK, obj.bookmark)
|
||||||
put(COL_DATE_FETCH, obj.date_fetch)
|
put(COL_DATE_FETCH, obj.date_fetch)
|
||||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||||
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
|||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
|
@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_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_LAST_READ
|
||||||
@ -44,7 +45,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
|
|
||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = History().apply {
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
||||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
||||||
|
@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
|
|||||||
|
|
||||||
override lateinit var name: String
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
|
||||||
override var read: Boolean = false
|
override var read: Boolean = false
|
||||||
|
|
||||||
override var bookmark: Boolean = false
|
override var bookmark: Boolean = false
|
||||||
@ -29,9 +31,7 @@ class ChapterImpl : Chapter {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val chapter = other as Chapter
|
val chapter = other as Chapter
|
||||||
|
|
||||||
return url == chapter.url
|
return url == chapter.url
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
|
@ -5,27 +5,27 @@ import java.io.Serializable
|
|||||||
/**
|
/**
|
||||||
* Object containing the history statistics of a chapter
|
* Object containing the history statistics of a chapter
|
||||||
*/
|
*/
|
||||||
class History : Serializable {
|
interface History : Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of history object.
|
* Id of history object.
|
||||||
*/
|
*/
|
||||||
var id: Long? = null
|
var id: Long?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter id of history object.
|
* Chapter id of history object.
|
||||||
*/
|
*/
|
||||||
var chapter_id: Long = 0
|
var chapter_id: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last time chapter was read in time long format
|
* Last time chapter was read in time long format
|
||||||
*/
|
*/
|
||||||
var last_read: Long = 0
|
var last_read: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total time chapter was read - todo not yet implemented
|
* Total time chapter was read - todo not yet implemented
|
||||||
*/
|
*/
|
||||||
var time_read: Long = 0
|
var time_read: Long
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -35,10 +35,8 @@ class History : Serializable {
|
|||||||
* @param chapter chapter object
|
* @param chapter chapter object
|
||||||
* @return history object
|
* @return history object
|
||||||
*/
|
*/
|
||||||
fun create(chapter: Chapter): History {
|
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||||
val history = History()
|
this.chapter_id = chapter.id!!
|
||||||
history.chapter_id = chapter.id!!
|
|
||||||
return history
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing the history statistics of a chapter
|
||||||
|
*/
|
||||||
|
class HistoryImpl : History {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of history object.
|
||||||
|
*/
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id of history object.
|
||||||
|
*/
|
||||||
|
override var chapter_id: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time chapter was read in time long format
|
||||||
|
*/
|
||||||
|
override var last_read: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time chapter was read - todo not yet implemented
|
||||||
|
*/
|
||||||
|
override var time_read: Long = 0
|
||||||
|
}
|
@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
* @param chapter object containing chater
|
* @param chapter object containing chater
|
||||||
* @param history object containing history
|
* @param history object containing history
|
||||||
*/
|
*/
|
||||||
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||||
|
@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
@ -42,6 +43,16 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapter(url: String) = db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
|
.whereArgs(url)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|
||||||
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||||
@ -50,6 +61,11 @@ interface ChapterQueries : DbProvider {
|
|||||||
|
|
||||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||||
|
|
||||||
|
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||||
|
.objects(chapters)
|
||||||
|
.withPutResolver(ChapterBackupPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||||
.`object`(chapter)
|
.`object`(chapter)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
@ -40,6 +41,15 @@ interface HistoryQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
||||||
|
.`object`(History::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getHistoryByChapterUrl())
|
||||||
|
.args(chapterUrl)
|
||||||
|
.observesTables(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the history last read.
|
* Updates the history last read.
|
||||||
* Inserts history object if not yet in database
|
* Inserts history object if not yet in database
|
||||||
@ -59,4 +69,18 @@ interface HistoryQueries : DbProvider {
|
|||||||
.objects(historyList)
|
.objects(historyList)
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
.withPutResolver(HistoryLastReadPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteHistory() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteHistoryNoLastRead() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||||
|
.whereArgs(0)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
@ -25,7 +26,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(RawQuery.builder()
|
||||||
.query(libraryQuery)
|
.query(libraryQuery)
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||||
.build())
|
.build())
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
@ -84,6 +85,12 @@ interface MangaQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteMangas() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getLastReadManga() = db.get()
|
fun getLastReadManga() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(RawQuery.builder()
|
||||||
@ -91,4 +98,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
||||||
}
|
}
|
@ -73,6 +73,14 @@ fun getHistoryByMangaId() = """
|
|||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getHistoryByChapterUrl() = """
|
||||||
|
SELECT ${History.TABLE}.*
|
||||||
|
FROM ${History.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
fun getLastReadMangaQuery() = """
|
fun getLastReadMangaQuery() = """
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
@ -85,6 +93,15 @@ fun getLastReadMangaQuery() = """
|
|||||||
ORDER BY max DESC
|
ORDER BY max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getTotalChapterMangaQuery()= """
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
ORDER by COUNT(*)
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
|
||||||
|
class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
|
.whereArgs(chapter.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||||
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ object ChapterTable {
|
|||||||
|
|
||||||
const val COL_READ = "read"
|
const val COL_READ = "read"
|
||||||
|
|
||||||
|
const val COL_SCANLATOR = "scanlator"
|
||||||
|
|
||||||
const val COL_BOOKMARK = "bookmark"
|
const val COL_BOOKMARK = "bookmark"
|
||||||
|
|
||||||
const val COL_DATE_FETCH = "date_fetch"
|
const val COL_DATE_FETCH = "date_fetch"
|
||||||
@ -32,6 +34,7 @@ object ChapterTable {
|
|||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_URL TEXT NOT NULL,
|
$COL_URL TEXT NOT NULL,
|
||||||
$COL_NAME TEXT NOT NULL,
|
$COL_NAME TEXT NOT NULL,
|
||||||
|
$COL_SCANLATOR TEXT,
|
||||||
$COL_READ BOOLEAN NOT NULL,
|
$COL_READ BOOLEAN NOT NULL,
|
||||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
$COL_BOOKMARK BOOLEAN NOT NULL,
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||||
@ -52,4 +55,7 @@ object ChapterTable {
|
|||||||
val bookmarkUpdateQuery: String
|
val bookmarkUpdateQuery: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
||||||
|
|
||||||
|
val addScanlator: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||||
|
|
||||||
|
// Show download notification when simultaneous download > 1.
|
||||||
|
notifier.onProgressChange(queue)
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
downloadsRelay.call(pending)
|
||||||
return !pending.isEmpty()
|
return !pending.isEmpty()
|
||||||
}
|
}
|
||||||
@ -380,7 +383,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
.map { response ->
|
.map { response ->
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.createFile("$filename.tmp")
|
||||||
try {
|
try {
|
||||||
response.body().source().saveTo(file.openOutputStream())
|
response.body()!!.source().saveTo(file.openOutputStream())
|
||||||
val extension = getImageExtension(response, file)
|
val extension = getImageExtension(response, file)
|
||||||
file.renameTo("$filename.$extension")
|
file.renameTo("$filename.$extension")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -403,7 +406,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
*/
|
*/
|
||||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||||
// Read content type if available.
|
// Read content type if available.
|
||||||
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||||
// Else guess from the uri.
|
// Else guess from the uri.
|
||||||
?: context.contentResolver.getType(file.uri)
|
?: context.contentResolver.getType(file.uri)
|
||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
@ -20,6 +22,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
|||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -28,7 +31,9 @@ import eu.kanade.tachiyomi.util.*
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
@ -40,27 +45,13 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
* progress of the update, and if case of an unexpected error, this service will be silently
|
* progress of the update, and if case of an unexpected error, this service will be silently
|
||||||
* destroyed.
|
* destroyed.
|
||||||
*/
|
*/
|
||||||
class LibraryUpdateService : Service() {
|
class LibraryUpdateService(
|
||||||
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
/**
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
* Database helper.
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
*/
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val db: DatabaseHelper by injectLazy()
|
val trackManager: TrackManager = Injekt.get()
|
||||||
|
) : Service() {
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences.
|
|
||||||
*/
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download Manager
|
|
||||||
*/
|
|
||||||
val downloadManager: DownloadManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
@ -75,29 +66,47 @@ class LibraryUpdateService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Pending intent of action that cancels the library update
|
* Pending intent of action that cancels the library update
|
||||||
*/
|
*/
|
||||||
private val cancelPendingIntent by lazy {NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)}
|
private val cancelIntent by lazy {
|
||||||
|
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the library update notification.
|
* Bitmap of the app for notifications.
|
||||||
*/
|
*/
|
||||||
private val notificationId: Int
|
|
||||||
get() = Constants.NOTIFICATION_LIBRARY_ID
|
|
||||||
|
|
||||||
private val notificationBitmap by lazy {
|
private val notificationBitmap by lazy {
|
||||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached progress notification to avoid creating a lot.
|
||||||
|
*/
|
||||||
|
private val progressNotification by lazy { NotificationCompat.Builder(this)
|
||||||
|
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
.setLargeIcon(notificationBitmap)
|
||||||
|
.setOngoing(true)
|
||||||
|
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines what should be updated within a service execution.
|
||||||
|
*/
|
||||||
|
enum class Target {
|
||||||
|
CHAPTERS, // Manga chapters
|
||||||
|
DETAILS, // Manga metadata
|
||||||
|
TRACKING // Tracking metadata
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_CATEGORY = "category"
|
const val KEY_CATEGORY = "category"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for updating the details instead of the chapters.
|
* Key that defines what should be updated.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_DETAILS = "details"
|
const val KEY_TARGET = "target"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
@ -106,7 +115,7 @@ class LibraryUpdateService : Service() {
|
|||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isRunning(context: Context): Boolean {
|
fun isRunning(context: Context): Boolean {
|
||||||
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
|
return context.isServiceRunning(LibraryUpdateService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,13 +124,13 @@ class LibraryUpdateService : Service() {
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param category a specific category to update, or null for global update.
|
* @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.
|
* @param target defines what should be updated.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, details: Boolean = false) {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(UPDATE_DETAILS, details)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
@ -144,16 +153,20 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
createAndAcquireWakeLock()
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
||||||
|
wakeLock.acquire()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is destroyed. It destroys the running subscription, resets
|
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
|
||||||
* the alarm and release the wake lock.
|
* lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
destroyWakeLock()
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +187,8 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
|
?: return Service.START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
@ -181,18 +196,19 @@ class LibraryUpdateService : Service() {
|
|||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable
|
subscription = Observable
|
||||||
.defer {
|
.defer {
|
||||||
val mangaList = getMangaToUpdate(intent)
|
val mangaList = getMangaToUpdate(intent, target)
|
||||||
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
|
when (target) {
|
||||||
updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
else
|
Target.DETAILS -> updateDetails(mangaList)
|
||||||
updateDetails(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
}, {
|
}, {
|
||||||
showNotification(getString(R.string.notification_update_error), "")
|
Timber.e(it)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
@ -205,15 +221,16 @@ class LibraryUpdateService : Service() {
|
|||||||
* Returns the list of manga to be updated.
|
* Returns the list of manga to be updated.
|
||||||
*
|
*
|
||||||
* @param intent the update intent.
|
* @param intent the update intent.
|
||||||
|
* @param target the target to update.
|
||||||
* @return a list of manga to update
|
* @return a list of manga to update
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent): List<Manga> {
|
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> {
|
||||||
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1)
|
var listToUpdate = if (categoryId != -1)
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
else {
|
else {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
|
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty())
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
.filter { it.category in categoriesToUpdate }
|
.filter { it.category in categoriesToUpdate }
|
||||||
@ -222,7 +239,7 @@ class LibraryUpdateService : Service() {
|
|||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +272,7 @@ class LibraryUpdateService : Service() {
|
|||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
@ -267,11 +284,11 @@ class LibraryUpdateService : Service() {
|
|||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
.filter { pair -> pair.first.isNotEmpty() }
|
.filter { pair -> pair.first.isNotEmpty() }
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
if (downloadNew) {
|
if (downloadNew && (categoriesToDownload.isEmpty() ||
|
||||||
if (categoriesToDownload.isEmpty() || manga.category in categoriesToDownload) {
|
manga.category in categoriesToDownload)) {
|
||||||
downloadChapters(manga, it.first)
|
|
||||||
hasDownloads = true
|
downloadChapters(manga, it.first)
|
||||||
}
|
hasDownloads = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Convert to the manga that contains new chapters.
|
// Convert to the manga that contains new chapters.
|
||||||
@ -287,16 +304,18 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
// Notify result of the overall update.
|
// Notify result of the overall update.
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
if (newUpdates.isEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
cancelNotification()
|
showResultNotification(newUpdates)
|
||||||
} else {
|
if (downloadNew && hasDownloads) {
|
||||||
showResultNotification(newUpdates, failedUpdates)
|
DownloadService.start(this)
|
||||||
if (downloadNew) {
|
|
||||||
if (hasDownloads) {
|
|
||||||
DownloadService.start(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedUpdates.isNotEmpty()) {
|
||||||
|
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,8 +343,6 @@ class LibraryUpdateService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Method that updates the details of the given list of manga. It's called in a background
|
* 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.
|
* 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
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
@ -337,7 +354,7 @@ class LibraryUpdateService : Service() {
|
|||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
@ -352,71 +369,44 @@ class LibraryUpdateService : Service() {
|
|||||||
.onErrorReturn { manga }
|
.onErrorReturn { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text that will be displayed in the notification when there are new chapters.
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
*
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
* @param updates a list of manga that contains new chapters.
|
|
||||||
* @param failedUpdates a list of manga that failed to update.
|
|
||||||
* @return the body of the notification to display.
|
|
||||||
*/
|
*/
|
||||||
private fun getUpdatedMangasBody(updates: List<Manga>, failedUpdates: List<Manga>): String {
|
private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||||
return buildString {
|
// Initialize the variables holding the progress of the updates.
|
||||||
if (updates.isEmpty()) {
|
var count = 0
|
||||||
append(getString(R.string.notification_no_new_chapters))
|
|
||||||
append("\n")
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
} else {
|
|
||||||
append(getString(R.string.notification_new_chapters))
|
// Emit each manga and update it sequentially.
|
||||||
for (manga in updates) {
|
return Observable.from(mangaToUpdate)
|
||||||
append("\n")
|
// Notify manga that will update.
|
||||||
append(manga.title.chop(45))
|
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||||
|
// Update the tracking details.
|
||||||
|
.concatMap { manga ->
|
||||||
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
Observable.from(tracks)
|
||||||
|
.concatMap { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service in loggedServices) {
|
||||||
|
service.refresh(track)
|
||||||
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
|
.onErrorReturn { track }
|
||||||
|
} else {
|
||||||
|
Observable.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { manga }
|
||||||
}
|
}
|
||||||
}
|
.doOnCompleted {
|
||||||
if (!failedUpdates.isEmpty()) {
|
cancelProgressNotification()
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.notification_manga_update_failed))
|
|
||||||
for (manga in failedUpdates) {
|
|
||||||
append("\n")
|
|
||||||
append(manga.title.chop(45))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and acquires a wake lock until the library is updated.
|
|
||||||
*/
|
|
||||||
private fun createAndAcquireWakeLock() {
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
|
||||||
wakeLock.acquire()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the wake lock if it's held.
|
|
||||||
*/
|
|
||||||
private fun destroyWakeLock() {
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
wakeLock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the notification with the given title and body.
|
|
||||||
*
|
|
||||||
* @param title the title of the notification.
|
|
||||||
* @param body the body of the notification.
|
|
||||||
*/
|
|
||||||
private fun showNotification(title: String, body: String) {
|
|
||||||
notificationManager.notify(notificationId, notification {
|
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
|
||||||
setLargeIcon(notificationBitmap)
|
|
||||||
setContentTitle(title)
|
|
||||||
setContentText(body)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -426,52 +416,67 @@ class LibraryUpdateService : Service() {
|
|||||||
* @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) {
|
||||||
notificationManager.notify(notificationId, notification {
|
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
.setContentTitle(manga.title)
|
||||||
setLargeIcon(notificationBitmap)
|
.setProgress(total, current, false)
|
||||||
setContentTitle(manga.title)
|
.build())
|
||||||
setProgress(total, current, false)
|
|
||||||
setOngoing(true)
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
private fun showResultNotification(updates: List<Manga>, failed: List<Manga>) {
|
private fun showResultNotification(updates: List<Manga>) {
|
||||||
val title = getString(R.string.notification_update_completed)
|
val newUpdates = updates.map { it.title.chop(45) }.toMutableSet()
|
||||||
val body = getUpdatedMangasBody(updates, failed)
|
|
||||||
|
|
||||||
notificationManager.notify(notificationId, notification {
|
// Append new chapters from a previous, existing notification
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val previousNotification = notificationManager.activeNotifications
|
||||||
|
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID }
|
||||||
|
|
||||||
|
if (previousNotification != null) {
|
||||||
|
val oldUpdates = previousNotification.notification.extras
|
||||||
|
.getString(Notification.EXTRA_BIG_TEXT)
|
||||||
|
|
||||||
|
if (!oldUpdates.isNullOrEmpty()) {
|
||||||
|
newUpdates += oldUpdates.split("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification {
|
||||||
|
setSmallIcon(R.drawable.ic_book_white_24dp)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(getString(R.string.notification_new_chapters))
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
if (newUpdates.size > 1) {
|
||||||
setContentIntent(notificationIntent)
|
setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
|
||||||
|
} else {
|
||||||
|
setContentText(newUpdates.first())
|
||||||
|
}
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
setContentIntent(getNotificationIntent())
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the notification.
|
* Cancels the progress notification.
|
||||||
*/
|
*/
|
||||||
private fun cancelNotification() {
|
private fun cancelProgressNotification() {
|
||||||
notificationManager.cancel(notificationId)
|
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property that returns an intent to open the main activity.
|
* Returns an intent to open the main activity.
|
||||||
*/
|
*/
|
||||||
private val notificationIntent: PendingIntent
|
private fun getNotificationIntent(): PendingIntent {
|
||||||
get() {
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.content.FileProvider
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -19,8 +17,9 @@ object NotificationHandler {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
*/
|
*/
|
||||||
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||||
val intent = Intent(context, DownloadActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
|
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
@ -33,7 +32,7 @@ object NotificationHandler {
|
|||||||
*/
|
*/
|
||||||
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
val uri = file.getUriCompat(context)
|
||||||
setDataAndType(uri, "image/*")
|
setDataAndType(uri, "image/*")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
@ -45,7 +48,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
// Cancel library update and dismiss notification
|
// Cancel library update and dismiss notification
|
||||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_ID)
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
@ -118,7 +121,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
// Delete file
|
// Delete file
|
||||||
val file = File(path)
|
val file = File(path)
|
||||||
file.deleteIfExists()
|
file.delete()
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, file)
|
DiskUtil.scanMedia(context, file)
|
||||||
}
|
}
|
||||||
@ -274,4 +277,4 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,112 +1,118 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
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.
|
||||||
|
*/
|
||||||
/**
|
object PreferenceKeys {
|
||||||
* 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
|
const val theme = "pref_theme_key"
|
||||||
* referenced here.
|
|
||||||
*/
|
const val rotation = "pref_rotation_type_key"
|
||||||
@Suppress("HasPlatformType")
|
|
||||||
class PreferenceKeys(context: Context) {
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
val theme = context.getString(R.string.pref_theme_key)
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
val rotation = context.getString(R.string.pref_rotation_type_key)
|
const val fullscreen = "fullscreen"
|
||||||
|
|
||||||
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
|
const val keepScreenOn = "pref_keep_screen_on_key"
|
||||||
|
|
||||||
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
|
const val customBrightness = "pref_custom_brightness_key"
|
||||||
|
|
||||||
val fullscreen = context.getString(R.string.pref_fullscreen_key)
|
const val customBrightnessValue = "custom_brightness_value"
|
||||||
|
|
||||||
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
|
const val colorFilter = "pref_color_filter_key"
|
||||||
|
|
||||||
val customBrightness = context.getString(R.string.pref_custom_brightness_key)
|
const val colorFilterValue = "color_filter_value"
|
||||||
|
|
||||||
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
const val defaultViewer = "pref_default_viewer_key"
|
||||||
|
|
||||||
val colorFilter = context.getString(R.string.pref_color_filter_key)
|
const val imageScaleType = "pref_image_scale_type_key"
|
||||||
|
|
||||||
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
|
const val imageDecoder = "image_decoder"
|
||||||
|
|
||||||
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
const val zoomStart = "pref_zoom_start_key"
|
||||||
|
|
||||||
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
|
const val readerTheme = "pref_reader_theme_key"
|
||||||
|
|
||||||
val imageDecoder = context.getString(R.string.pref_image_decoder_key)
|
const val cropBorders = "crop_borders"
|
||||||
|
|
||||||
val zoomStart = context.getString(R.string.pref_zoom_start_key)
|
const val readWithTapping = "reader_tap"
|
||||||
|
|
||||||
val readerTheme = context.getString(R.string.pref_reader_theme_key)
|
const val readWithVolumeKeys = "reader_volume_keys"
|
||||||
|
|
||||||
val cropBorders = context.getString(R.string.pref_crop_borders_key)
|
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
||||||
|
|
||||||
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
|
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||||
|
|
||||||
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
|
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||||
|
|
||||||
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key)
|
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||||
|
|
||||||
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key)
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
const val askUpdateTrack = "pref_ask_update_manga_sync_key"
|
||||||
|
|
||||||
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
|
const val lastUsedCatalogueSource = "last_catalogue_source"
|
||||||
|
|
||||||
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
|
const val lastUsedCategory = "last_used_category"
|
||||||
|
|
||||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
const val catalogueAsList = "pref_display_catalogue_as_list"
|
||||||
|
|
||||||
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
const val enabledLanguages = "source_languages"
|
||||||
|
|
||||||
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
|
const val backupDirectory = "backup_directory"
|
||||||
|
|
||||||
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
const val downloadsDirectory = "download_directory"
|
||||||
|
|
||||||
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
|
const val downloadThreads = "pref_download_slots_key"
|
||||||
|
|
||||||
val downloadThreads = context.getString(R.string.pref_download_slots_key)
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
const val numberOfBackups = "backup_slots"
|
||||||
|
|
||||||
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
const val backupInterval = "backup_interval"
|
||||||
|
|
||||||
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
const val removeAfterReadSlots = "remove_after_read_slots"
|
||||||
|
|
||||||
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key)
|
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
|
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||||
|
|
||||||
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
|
|
||||||
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
const val libraryUpdateCategories = "library_update_categories"
|
||||||
|
|
||||||
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||||
|
|
||||||
val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key)
|
const val filterUnread = "pref_filter_unread_key"
|
||||||
|
|
||||||
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
const val filterCompleted = "pref_filter_completed_key"
|
||||||
|
|
||||||
val startScreen = context.getString(R.string.pref_start_screen_key)
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
|
||||||
val downloadNew = context.getString(R.string.pref_download_new_key)
|
const val automaticUpdates = "automatic_updates"
|
||||||
|
|
||||||
val downloadNewCategories = context.getString(R.string.pref_download_new_categories_key)
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
const val libraryAsList = "pref_display_library_as_list"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
const val lang = "app_language"
|
||||||
|
|
||||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
val lang = context.getString(R.string.pref_language_key)
|
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
}
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
|
|
||||||
@ -17,8 +18,6 @@ fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
|||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
val keys = PreferenceKeys(context)
|
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val rxPrefs = RxSharedPreferences.create(prefs)
|
private val rxPrefs = RxSharedPreferences.create(prefs)
|
||||||
|
|
||||||
@ -26,126 +25,142 @@ class PreferencesHelper(val context: Context) {
|
|||||||
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
context.getString(R.string.app_name), "downloads"))
|
context.getString(R.string.app_name), "downloads"))
|
||||||
|
|
||||||
fun startScreen() = prefs.getInt(keys.startScreen, 1)
|
private val defaultBackupDir = Uri.fromFile(
|
||||||
|
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
|
context.getString(R.string.app_name), "backup"))
|
||||||
|
|
||||||
|
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||||
|
|
||||||
fun clear() = prefs.edit().clear().apply()
|
fun clear() = prefs.edit().clear().apply()
|
||||||
|
|
||||||
fun theme() = prefs.getInt(keys.theme, 1)
|
fun theme() = prefs.getInt(Keys.theme, 1)
|
||||||
|
|
||||||
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1)
|
||||||
|
|
||||||
fun pageTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
|
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
|
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
||||||
|
|
||||||
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
|
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
||||||
|
|
||||||
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
|
fun customBrightness() = rxPrefs.getBoolean(Keys.customBrightness, false)
|
||||||
|
|
||||||
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
|
fun customBrightnessValue() = rxPrefs.getInteger(Keys.customBrightnessValue, 0)
|
||||||
|
|
||||||
fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
|
fun colorFilter() = rxPrefs.getBoolean(Keys.colorFilter, false)
|
||||||
|
|
||||||
fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
|
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
|
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
||||||
|
|
||||||
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
|
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
||||||
|
|
||||||
fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0)
|
fun imageDecoder() = rxPrefs.getInteger(Keys.imageDecoder, 0)
|
||||||
|
|
||||||
fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1)
|
fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1)
|
||||||
|
|
||||||
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
|
fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0)
|
||||||
|
|
||||||
fun cropBorders() = rxPrefs.getBoolean(keys.cropBorders, false)
|
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
|
||||||
|
|
||||||
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
|
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||||
|
|
||||||
fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0)
|
fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
||||||
|
|
||||||
fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0)
|
fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0)
|
||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0)
|
||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
|
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||||
|
|
||||||
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1)
|
fun askUpdateTrack() = prefs.getBoolean(Keys.askUpdateTrack, false)
|
||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
|
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false)
|
||||||
|
|
||||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("en"))
|
fun enabledLanguages() = rxPrefs.getStringSet(Keys.enabledLanguages, setOf("en"))
|
||||||
|
|
||||||
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
|
fun sourceUsername(source: Source) = prefs.getString(Keys.sourceUsername(source.id), "")
|
||||||
|
|
||||||
fun sourcePassword(source: Source) = prefs.getString(keys.sourcePassword(source.id), "")
|
fun sourcePassword(source: Source) = prefs.getString(Keys.sourcePassword(source.id), "")
|
||||||
|
|
||||||
fun setSourceCredentials(source: Source, username: String, password: String) {
|
fun setSourceCredentials(source: Source, username: String, password: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(keys.sourceUsername(source.id), username)
|
.putString(Keys.sourceUsername(source.id), username)
|
||||||
.putString(keys.sourcePassword(source.id), password)
|
.putString(Keys.sourcePassword(source.id), password)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
|
fun trackPassword(sync: TrackService) = prefs.getString(Keys.trackPassword(sync.id), "")
|
||||||
|
|
||||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(keys.trackUsername(sync.id), username)
|
.putString(Keys.trackUsername(sync.id), username)
|
||||||
.putString(keys.trackPassword(sync.id), password)
|
.putString(Keys.trackPassword(sync.id), password)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
|
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
||||||
|
|
||||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||||
|
|
||||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
fun downloadThreads() = rxPrefs.getInteger(Keys.downloadThreads, 1)
|
||||||
|
|
||||||
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1)
|
||||||
|
|
||||||
fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0)
|
fun backupInterval() = rxPrefs.getInteger(Keys.backupInterval, 0)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
|
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet())
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||||
|
|
||||||
fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
|
fun libraryUpdateInterval() = rxPrefs.getInteger(Keys.libraryUpdateInterval, 0)
|
||||||
|
|
||||||
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
|
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet())
|
||||||
|
|
||||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
|
|
||||||
fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0)
|
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||||
|
|
||||||
|
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||||
|
|
||||||
|
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
|
||||||
|
|
||||||
|
fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false)
|
||||||
|
|
||||||
|
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
|
||||||
|
|
||||||
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
||||||
|
|
||||||
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, false)
|
||||||
|
|
||||||
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = rxPrefs.getBoolean(keys.downloadNew, false)
|
fun downloadNew() = rxPrefs.getBoolean(Keys.downloadNew, false)
|
||||||
|
|
||||||
fun downloadNewCategories() = rxPrefs.getStringSet(keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = rxPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||||
|
|
||||||
fun lang() = prefs.getString(keys.lang, "")
|
fun lang() = prefs.getString(Keys.lang, "")
|
||||||
|
|
||||||
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
fun addLibManga(track: Track): Observable<Track> {
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
||||||
.map { response ->
|
.map { response ->
|
||||||
response.body().close()
|
response.body()?.close()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw Exception("Could not add manga")
|
throw Exception("Could not add manga")
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
||||||
track.toAnilistScore())
|
track.toAnilistScore())
|
||||||
.map { response ->
|
.map { response ->
|
||||||
response.body().close()
|
response.body()?.close()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw Exception("Could not update manga")
|
throw Exception("Could not update manga")
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
|||||||
if (oauth == null || oauth!!.isExpired()) {
|
if (oauth == null || oauth!!.isExpired()) {
|
||||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||||
oauth = if (response.isSuccessful) {
|
oauth = if (response.isSuccessful) {
|
||||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
null
|
null
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
|||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
@ -55,11 +56,17 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
return IntRange(0, 10).map { (it.toFloat() / 2).toString() }
|
val df = DecimalFormat("0.#")
|
||||||
|
return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun indexToScore(index: Int): Float {
|
||||||
|
return if (index > 0) (index + 1) / 2f else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: Track): String {
|
||||||
return track.toKitsuScore()
|
val df = DecimalFormat("0.#")
|
||||||
|
return df.format(track.score)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.*
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@ -17,7 +18,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
private val rest = Retrofit.Builder()
|
private val rest = Retrofit.Builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create(KitsuApi.Rest::class.java)
|
.create(KitsuApi.Rest::class.java)
|
||||||
@ -65,7 +66,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
"attributes" to jsonObject(
|
"attributes" to jsonObject(
|
||||||
"status" to track.toKitsuStatus(),
|
"status" to track.toKitsuStatus(),
|
||||||
"progress" to track.last_chapter_read,
|
"progress" to track.last_chapter_read,
|
||||||
"rating" to track.toKitsuScore()
|
"ratingTwenty" to track.toKitsuScore()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
@ -148,9 +149,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
|
|
||||||
@GET("library-entries")
|
@GET("library-entries")
|
||||||
fun findLibManga(
|
fun findLibManga(
|
||||||
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||||
@Query("filter[user_id]", encoded = true) userId: String,
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
|
||||||
@Query("include") includes: String = "manga"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
|||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
|
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,13 @@ open class KitsuManga(obj: JsonObject) {
|
|||||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
||||||
val remoteId by obj.byInt("id")
|
val remoteId by obj.byInt("id")
|
||||||
val status by obj["attributes"].byString
|
val status by obj["attributes"].byString
|
||||||
val rating = obj["attributes"].obj.get("rating").nullString
|
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
||||||
val progress by obj["attributes"].byInt
|
val progress by obj["attributes"].byInt
|
||||||
|
|
||||||
override fun toTrack() = super.toTrack().apply {
|
override fun toTrack() = super.toTrack().apply {
|
||||||
remote_id = remoteId
|
remote_id = remoteId
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
score = rating?.let { it.toFloat() * 2 } ?: 0f
|
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||||
last_chapter_read = progress
|
last_chapter_read = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +53,6 @@ fun Track.toKitsuStatus() = when (status) {
|
|||||||
else -> throw Exception("Unknown status")
|
else -> throw Exception("Unknown status")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Track.toKitsuScore(): String {
|
fun Track.toKitsuScore(): String? {
|
||||||
return if (score > 0) (score / 2).toString() else ""
|
return if (score > 0) (score * 2).toInt().toString() else null
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
} else {
|
} else {
|
||||||
client.newCall(GET(getSearchUrl(query), headers))
|
client.newCall(GET(getSearchUrl(query), headers))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
.map { Jsoup.parse(it.body()!!.string()) }
|
||||||
.flatMap { Observable.from(it.select("entry")) }
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
.filter { it.select("type").text() != "Novel" }
|
.filter { it.select("type").text() != "Novel" }
|
||||||
.map {
|
.map {
|
||||||
@ -64,7 +64,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
return client
|
return client
|
||||||
.newCall(GET(getListUrl(username), headers))
|
.newCall(GET(getListUrl(username), headers))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
.map { Jsoup.parse(it.body()!!.string()) }
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
.map {
|
.map {
|
||||||
Track.create(TrackManager.MYANIMELIST).apply {
|
Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
@ -86,7 +86,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
val apkFile = File(externalCacheDir, "update.apk")
|
val apkFile = File(externalCacheDir, "update.apk")
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body().source().saveTo(apkFile)
|
response.body()!!.source().saveTo(apkFile)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
throw Exception("Unsuccessful response")
|
throw Exception("Unsuccessful response")
|
||||||
|
@ -8,13 +8,10 @@ import okhttp3.Response
|
|||||||
|
|
||||||
class CloudflareInterceptor : Interceptor {
|
class CloudflareInterceptor : Interceptor {
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
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="(.+?)"""")
|
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -34,7 +31,7 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
val originalRequest = response.request()
|
val originalRequest = response.request()
|
||||||
val url = originalRequest.url()
|
val url = originalRequest.url()
|
||||||
val domain = url.host()
|
val domain = url.host()
|
||||||
val content = response.body().string()
|
val content = response.body()!!.string()
|
||||||
|
|
||||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||||
Thread.sleep(4000)
|
Thread.sleep(4000)
|
||||||
@ -48,9 +45,7 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val js = operation
|
val js = operation
|
||||||
//language=RegExp
|
|
||||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||||
//language=RegExp
|
|
||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
|
|
||||||
@ -58,7 +53,7 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
|
|
||||||
val answer = "${result + domain.length}"
|
val answer = "${result + domain.length}"
|
||||||
|
|
||||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")
|
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!!
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addQueryParameter("jschl_vc", challenge)
|
.addQueryParameter("jschl_vc", challenge)
|
||||||
.addQueryParameter("pass", pass)
|
.addQueryParameter("pass", pass)
|
||||||
|
@ -61,7 +61,7 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
|||||||
.addNetworkInterceptor { chain ->
|
.addNetworkInterceptor { chain ->
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
.body(ProgressResponseBody(originalResponse.body()!!, listener))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
@ -18,7 +18,7 @@ class PersistentCookieStore(context: Context) {
|
|||||||
if (cookies != null) {
|
if (cookies != null) {
|
||||||
try {
|
try {
|
||||||
val url = HttpUrl.parse("http://$key")
|
val url = HttpUrl.parse("http://$key")
|
||||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||||
.filter { !it.hasExpired() }
|
.filter { !it.hasExpired() }
|
||||||
cookieMap.put(key, nonExpiredCookies)
|
cookieMap.put(key, nonExpiredCookies)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -12,7 +12,7 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun contentType(): MediaType {
|
override fun contentType(): MediaType {
|
||||||
return responseBody.contentType()
|
return responseBody.contentType()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentLength(): Long {
|
override fun contentLength(): Long {
|
||||||
|
@ -57,7 +57,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override val id = ID
|
override val id = ID
|
||||||
override val name = "LocalSource"
|
override val name = "LocalSource"
|
||||||
override val lang = "en"
|
override val lang = ""
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun toString() = context.getString(R.string.local_source)
|
override fun toString() = context.getString(R.string.local_source)
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory for creating sources at runtime.
|
||||||
|
*/
|
||||||
|
interface SourceFactory {
|
||||||
|
/**
|
||||||
|
* Create a new copy of the sources
|
||||||
|
* @return The created sources
|
||||||
|
*/
|
||||||
|
fun createSources(): List<Source>
|
||||||
|
}
|
@ -74,7 +74,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
||||||
sources.add(YamlHttpSource(map))
|
sources.add(YamlHttpSource(map))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Error loading source from file. Bad format?")
|
Timber.e("Error loading source from file. Bad format?", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,25 +95,29 @@ open class SourceManager(private val context: Context) {
|
|||||||
val extName = pkgManager.getApplicationLabel(appInfo).toString()
|
val extName = pkgManager.getApplicationLabel(appInfo).toString()
|
||||||
.substringAfter("Tachiyomi: ")
|
.substringAfter("Tachiyomi: ")
|
||||||
val version = pkgInfo.versionName
|
val version = pkgInfo.versionName
|
||||||
var sourceClass = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
val sourceClasses = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
if (sourceClass.startsWith(".")) {
|
.split(";")
|
||||||
sourceClass = pkgInfo.packageName + sourceClass
|
.map {
|
||||||
}
|
val sourceClass = it.trim()
|
||||||
|
if(sourceClass.startsWith("."))
|
||||||
|
pkgInfo.packageName + sourceClass
|
||||||
|
else
|
||||||
|
sourceClass
|
||||||
|
}
|
||||||
|
|
||||||
val extension = Extension(extName, appInfo, version, sourceClass)
|
val extension = Extension(extName, appInfo, version, sourceClasses)
|
||||||
try {
|
try {
|
||||||
val instance = loadExtension(extension)
|
sources += loadExtension(extension)
|
||||||
sources.add(instance)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Extension load error: $extName. Reason: ${e.message}")
|
Timber.e("Extension load error: $extName.", e)
|
||||||
} catch (e: LinkageError) {
|
} catch (e: LinkageError) {
|
||||||
Timber.e("Extension load error: $extName. Reason: ${e.message}")
|
Timber.e("Extension load error: $extName.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadExtension(ext: Extension): Source {
|
private fun loadExtension(ext: Extension): List<Source> {
|
||||||
// Validate lib version
|
// Validate lib version
|
||||||
val majorLibVersion = ext.version.substringBefore('.').toInt()
|
val majorLibVersion = ext.version.substringBefore('.').toInt()
|
||||||
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
|
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
|
||||||
@ -122,13 +126,20 @@ open class SourceManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
|
val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
|
||||||
return Class.forName(ext.sourceClass, false, classLoader).newInstance() as Source
|
return ext.sourceClasses.flatMap {
|
||||||
|
val obj = Class.forName(it, false, classLoader).newInstance()
|
||||||
|
when(obj) {
|
||||||
|
is Source -> listOf(obj)
|
||||||
|
is SourceFactory -> obj.createSources()
|
||||||
|
else -> throw Exception("Unknown source class type!")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Extension(val name: String,
|
class Extension(val name: String,
|
||||||
val appInfo: ApplicationInfo,
|
val appInfo: ApplicationInfo,
|
||||||
val version: String,
|
val version: String,
|
||||||
val sourceClass: String)
|
val sourceClasses: List<String>)
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
|
@ -12,11 +12,14 @@ interface SChapter : Serializable {
|
|||||||
|
|
||||||
var chapter_number: Float
|
var chapter_number: Float
|
||||||
|
|
||||||
|
var scanlator: String?
|
||||||
|
|
||||||
fun copyFrom(other: SChapter) {
|
fun copyFrom(other: SChapter) {
|
||||||
name = other.name
|
name = other.name
|
||||||
url = other.url
|
url = other.url
|
||||||
date_upload = other.date_upload
|
date_upload = other.date_upload
|
||||||
chapter_number = other.chapter_number
|
chapter_number = other.chapter_number
|
||||||
|
scanlator = other.scanlator
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
|
|||||||
|
|
||||||
override var chapter_number: Float = -1f
|
override var chapter_number: Float = -1f
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
|
||||||
}
|
}
|
@ -13,6 +13,7 @@ import okhttp3.Request
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.lang.Exception
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
override val id by lazy {
|
override val id by lazy {
|
||||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||||
* override this method.
|
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||||
*
|
*
|
||||||
* @param manga the manga to look for chapters.
|
* @param manga the manga to look for chapters.
|
||||||
*/
|
*/
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return client.newCall(chapterListRequest(manga))
|
if (manga.status != SManga.LICENSED) {
|
||||||
.asObservableSuccess()
|
return client.newCall(chapterListRequest(manga))
|
||||||
.map { response ->
|
.asObservableSuccess()
|
||||||
chapterListParse(response)
|
.map { response ->
|
||||||
}
|
chapterListParse(response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Observable.error(Exception("Licensed - No chapters to show"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,7 +171,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
val url = response.request().url().toString()
|
val url = response.request().url().toString()
|
||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
@ -216,7 +216,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
val url = response.request().url().toString()
|
val url = response.request().url().toString()
|
||||||
|
|
||||||
with(map.pages) {
|
with(map.pages) {
|
||||||
|
@ -28,7 +28,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
|
|
||||||
override val name = "Batoto"
|
override val name = "Batoto"
|
||||||
|
|
||||||
override val baseUrl = "http://bato.to"
|
override val baseUrl = "https://bato.to"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
.add("Cookie", "lang_option=English")
|
.add("Cookie", "lang_option=English")
|
||||||
|
|
||||||
private val pageHeaders = super.headersBuilder()
|
private val pageHeaders = super.headersBuilder()
|
||||||
.add("Referer", "http://bato.to/reader")
|
.add("Referer", "$baseUrl/reader")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
@ -69,7 +69,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
element.select("a[href^=http://bato.to]").first().let {
|
element.select("a[href*=bato.to]").first().let {
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text().trim()
|
manga.title = it.text().trim()
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
val url = HttpUrl.parse("$baseUrl/search_ajax")!!.newBuilder()
|
||||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||||
var genres = ""
|
var genres = ""
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
@ -161,8 +161,20 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
// Https is currently very slow. The replace also saves a redirection.
|
||||||
|
var newUrl = "http://bato.to" + manga.url
|
||||||
|
if ("/comic/_/comics/" !in newUrl) {
|
||||||
|
newUrl = newUrl.replace("/comic/_/", "/comic/_/comics/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.chapterListRequest(manga).newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
val matcher = staffNotice.matcher(body)
|
val matcher = staffNotice.matcher(body)
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@ -177,7 +189,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
val urlElement = element.select("a[href*=bato.to/reader").first()
|
||||||
|
|
||||||
val chapter = SChapter.create()
|
val chapter = SChapter.create()
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
@ -185,6 +197,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||||
parseDateFromElement(it)
|
parseDateFromElement(it)
|
||||||
} ?: 0
|
} ?: 0
|
||||||
|
chapter.scanlator = element.select("td").getOrNull(2)?.text()
|
||||||
return chapter
|
return chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +284,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isAuthenticationSuccessful(response: Response) =
|
override fun isAuthenticationSuccessful(response: Response) =
|
||||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
response.priorResponse() != null && response.priorResponse()!!.code() == 302
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
override fun isLogged(): Boolean {
|
||||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||||
|
@ -115,13 +115,13 @@ class Kissmanga : ParsedHttpSource() {
|
|||||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
||||||
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body().string()
|
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body()!!.string()
|
||||||
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body().string()
|
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body()!!.string()
|
||||||
|
|
||||||
Duktape.create().use {
|
Duktape.create().use {
|
||||||
it.evaluate(ca)
|
it.evaluate(ca)
|
||||||
|
@ -55,13 +55,13 @@ class Mangafox : ParsedHttpSource() {
|
|||||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||||
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
|
is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString())
|
||||||
is OrderBy -> {
|
is OrderBy -> {
|
||||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||||
@ -113,7 +113,7 @@ class Mangafox : ParsedHttpSource() {
|
|||||||
|
|
||||||
val chapter = SChapter.create()
|
val chapter = SChapter.create()
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.text()
|
chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text()
|
||||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
return chapter
|
return chapter
|
||||||
}
|
}
|
||||||
@ -169,6 +169,7 @@ class Mangafox : ParsedHttpSource() {
|
|||||||
private class OrderBy : Filter.Sort("Order by",
|
private class OrderBy : Filter.Sort("Order by",
|
||||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||||
Filter.Sort.Selection(2, false))
|
Filter.Sort.Selection(2, false))
|
||||||
|
|
||||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
|
@ -57,7 +57,7 @@ class Mangahere : ParsedHttpSource() {
|
|||||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||||
@ -152,6 +152,11 @@ class Mangahere : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val licensedError = document.select(".mangaread_error > .mt10").first()
|
||||||
|
if (licensedError != null) {
|
||||||
|
throw Exception(licensedError.text())
|
||||||
|
}
|
||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||||
pages.add(Page(pages.size, it.attr("value")))
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
|
@ -54,7 +54,7 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
override fun searchMangaSelector() = "div.requested > div.row"
|
override fun searchMangaSelector() = "div.requested > div.row"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
|
val url = HttpUrl.parse("$baseUrl/search/request.php")!!.newBuilder()
|
||||||
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||||
val genres = mutableListOf<String>()
|
val genres = mutableListOf<String>()
|
||||||
val genresNo = mutableListOf<String>()
|
val genresNo = mutableListOf<String>()
|
||||||
@ -84,7 +84,7 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||||
val url = HttpUrl.parse(url)
|
val url = HttpUrl.parse(url)!!
|
||||||
val body = FormBody.Builder().add("page", page.toString())
|
val body = FormBody.Builder().add("page", page.toString())
|
||||||
for (i in 0..url.querySize() - 1) {
|
for (i in 0..url.querySize() - 1) {
|
||||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||||
|
@ -152,7 +152,7 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body().string()
|
val html = response.body()!!.string()
|
||||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||||
val endIndex = html.indexOf(",]", beginIndex)
|
val endIndex = html.indexOf(",]", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||||
|
@ -120,7 +120,7 @@ class Mintmanga : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body().string()
|
val html = response.body()!!.string()
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
val beginIndex = html.indexOf("rm_h.init( [")
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||||
|
@ -120,7 +120,7 @@ class Readmanga : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body().string()
|
val html = response.body()!!.string()
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
val beginIndex = html.indexOf("rm_h.init( [")
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||||
|
@ -1,163 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.backup
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import kotlinx.android.synthetic.main.fragment_backup.*
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.internal.util.SubscriptionList
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment to create and restore backups of the application's data.
|
|
||||||
* Uses R.layout.fragment_backup.
|
|
||||||
*/
|
|
||||||
@RequiresPresenter(BackupPresenter::class)
|
|
||||||
class BackupFragment : BaseRxFragment<BackupPresenter>() {
|
|
||||||
|
|
||||||
private var backupDialog: Dialog? = null
|
|
||||||
private var restoreDialog: Dialog? = null
|
|
||||||
|
|
||||||
private lateinit var subscriptions: SubscriptionList
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
|
||||||
return inflater.inflate(R.layout.fragment_backup, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
setToolbarTitle(getString(R.string.label_backup))
|
|
||||||
|
|
||||||
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
|
|
||||||
subscriptions = SubscriptionList()
|
|
||||||
|
|
||||||
backup_button.setOnClickListener {
|
|
||||||
val today = SimpleDateFormat("yyyy-MM-dd").format(Date())
|
|
||||||
val file = File(activity.externalCacheDir, "tachiyomi-$today.json")
|
|
||||||
presenter.createBackup(file)
|
|
||||||
|
|
||||||
backupDialog = MaterialDialog.Builder(activity)
|
|
||||||
.content(R.string.backup_please_wait)
|
|
||||||
.progress(true, 0)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
restore_button.setOnClickListener {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
intent.type = "application/*"
|
|
||||||
val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup))
|
|
||||||
startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
subscriptions.unsubscribe()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when the backup is completed.
|
|
||||||
*
|
|
||||||
* @param file the file where the backup is saved.
|
|
||||||
*/
|
|
||||||
fun onBackupCompleted(file: File) {
|
|
||||||
dismissBackupDialog()
|
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
|
||||||
intent.type = "application/json"
|
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file))
|
|
||||||
startActivity(Intent.createChooser(intent, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when the restore is completed.
|
|
||||||
*/
|
|
||||||
fun onRestoreCompleted() {
|
|
||||||
dismissRestoreDialog()
|
|
||||||
context.toast(R.string.backup_completed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when there's an error doing the backup.
|
|
||||||
* @param error the exception thrown.
|
|
||||||
*/
|
|
||||||
fun onBackupError(error: Throwable) {
|
|
||||||
dismissBackupDialog()
|
|
||||||
context.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when there's an error restoring the backup.
|
|
||||||
* @param error the exception thrown.
|
|
||||||
*/
|
|
||||||
fun onRestoreError(error: Throwable) {
|
|
||||||
dismissRestoreDialog()
|
|
||||||
context.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_BACKUP_OPEN) {
|
|
||||||
restoreDialog = MaterialDialog.Builder(activity)
|
|
||||||
.content(R.string.restore_please_wait)
|
|
||||||
.progress(true, 0)
|
|
||||||
.show()
|
|
||||||
|
|
||||||
// When using cloud services, we have to open the input stream in a background thread.
|
|
||||||
Observable.fromCallable { context.contentResolver.openInputStream(data.data) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
presenter.restoreBackup(it)
|
|
||||||
}, { error ->
|
|
||||||
context.toast(error.message)
|
|
||||||
Timber.e(error)
|
|
||||||
})
|
|
||||||
.apply { subscriptions.add(this) }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismisses the backup dialog.
|
|
||||||
*/
|
|
||||||
fun dismissBackupDialog() {
|
|
||||||
backupDialog?.let {
|
|
||||||
it.dismiss()
|
|
||||||
backupDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismisses the restore dialog.
|
|
||||||
*/
|
|
||||||
fun dismissRestoreDialog() {
|
|
||||||
restoreDialog?.let {
|
|
||||||
it.dismiss()
|
|
||||||
restoreDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val REQUEST_BACKUP_OPEN = 102
|
|
||||||
|
|
||||||
fun newInstance(): BackupFragment {
|
|
||||||
return BackupFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.backup
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presenter of [BackupFragment].
|
|
||||||
*/
|
|
||||||
class BackupPresenter : BasePresenter<BackupFragment>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database.
|
|
||||||
*/
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup manager.
|
|
||||||
*/
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the backup is restored.
|
|
||||||
*/
|
|
||||||
private var restoreSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the backup is created.
|
|
||||||
*/
|
|
||||||
private var backupSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
backupManager = BackupManager(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a backup and saves it to a file.
|
|
||||||
*
|
|
||||||
* @param file the path where the file will be saved.
|
|
||||||
*/
|
|
||||||
fun createBackup(file: File) {
|
|
||||||
if (backupSubscription.isNullOrUnsubscribed()) {
|
|
||||||
backupSubscription = getBackupObservable(file)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, result -> view.onBackupCompleted(file) },
|
|
||||||
BackupFragment::onBackupError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores a backup from a stream.
|
|
||||||
*
|
|
||||||
* @param stream the input stream of the backup file.
|
|
||||||
*/
|
|
||||||
fun restoreBackup(stream: InputStream) {
|
|
||||||
if (restoreSubscription.isNullOrUnsubscribed()) {
|
|
||||||
restoreSubscription = getRestoreObservable(stream)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, result -> view.onRestoreCompleted() },
|
|
||||||
BackupFragment::onRestoreError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable to save a backup.
|
|
||||||
*/
|
|
||||||
private fun getBackupObservable(file: File) = Observable.fromCallable {
|
|
||||||
backupManager.backupToFile(file)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable to restore a backup.
|
|
||||||
*/
|
|
||||||
private fun getRestoreObservable(stream: InputStream) = Observable.fromCallable {
|
|
||||||
backupManager.restoreFromStream(stream)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v4.app.ActivityCompat
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.support.v7.app.ActionBar
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
interface ActivityMixin {
|
|
||||||
|
|
||||||
var resumed: Boolean
|
|
||||||
|
|
||||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
if (backNavigation) {
|
|
||||||
toolbar.setNavigationOnClickListener {
|
|
||||||
if (resumed) {
|
|
||||||
onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAppTheme() {
|
|
||||||
setTheme(when (Injekt.get<PreferencesHelper>().theme()) {
|
|
||||||
2 -> R.style.Theme_Tachiyomi_Dark
|
|
||||||
else -> R.style.Theme_Tachiyomi
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(title: String) {
|
|
||||||
getSupportActionBar()?.title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(titleResource: Int) {
|
|
||||||
getSupportActionBar()?.title = getString(titleResource)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarSubtitle(title: String) {
|
|
||||||
getSupportActionBar()?.subtitle = title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarSubtitle(titleResource: Int) {
|
|
||||||
getSupportActionBar()?.subtitle = getString(titleResource)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests read and write permissions on Android M and higher.
|
|
||||||
*/
|
|
||||||
fun requestPermissionsOnMarshmallow() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (ContextCompat.checkSelfPermission(getActivity(),
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
|
|
||||||
ActivityCompat.requestPermissions(getActivity(),
|
|
||||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
|
|
||||||
1)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getActivity(): AppCompatActivity
|
|
||||||
|
|
||||||
fun onBackPressed()
|
|
||||||
|
|
||||||
fun getSupportActionBar(): ActionBar?
|
|
||||||
|
|
||||||
fun setSupportActionBar(toolbar: Toolbar?)
|
|
||||||
|
|
||||||
fun setTheme(resource: Int)
|
|
||||||
|
|
||||||
fun getString(resource: Int): String
|
|
||||||
|
|
||||||
}
|
|
@ -3,24 +3,11 @@ package eu.kanade.tachiyomi.ui.base.activity
|
|||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override var resumed = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
LocaleHelper.updateConfiguration(this)
|
LocaleHelper.updateConfiguration(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActivity() = this
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
resumed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
resumed = false
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import nucleus.view.NucleusAppCompatActivity
|
import nucleus.view.NucleusAppCompatActivity
|
||||||
|
|
||||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>() {
|
||||||
|
|
||||||
override var resumed = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
LocaleHelper.updateConfiguration(this)
|
LocaleHelper.updateConfiguration(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
val superFactory = presenterFactory
|
|
||||||
setPresenterFactory {
|
|
||||||
superFactory.createPresenter().apply {
|
|
||||||
val app = application as App
|
|
||||||
context = app.applicationContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onCreate(savedState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getActivity() = this
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
resumed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
resumed = false
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
|
||||||
|
|
||||||
abstract class FlexibleViewHolder(view: View,
|
|
||||||
private val adapter: FlexibleAdapter<*, *>,
|
|
||||||
private val itemClickListener: FlexibleViewHolder.OnListItemClickListener) :
|
|
||||||
RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.setOnClickListener(this)
|
|
||||||
view.setOnLongClickListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(view: View) {
|
|
||||||
if (itemClickListener.onListItemClick(adapterPosition)) {
|
|
||||||
toggleActivation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(view: View): Boolean {
|
|
||||||
itemClickListener.onListItemLongClick(adapterPosition)
|
|
||||||
toggleActivation()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleActivation() {
|
|
||||||
itemView.isActivated = adapter.isSelected(adapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnListItemClickListener {
|
|
||||||
fun onListItemClick(position: Int): Boolean
|
|
||||||
fun onListItemLongClick(position: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v4.app.FragmentManager
|
|
||||||
import android.support.v4.app.FragmentStatePagerAdapter
|
|
||||||
import android.util.SparseArray
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class SmartFragmentStatePagerAdapter(fragmentManager: FragmentManager) :
|
|
||||||
FragmentStatePagerAdapter(fragmentManager) {
|
|
||||||
// Sparse array to keep track of registered fragments in memory
|
|
||||||
private val registeredFragments = SparseArray<Fragment>()
|
|
||||||
|
|
||||||
// Register the fragment when the item is instantiated
|
|
||||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
|
||||||
val fragment = super.instantiateItem(container, position) as Fragment
|
|
||||||
registeredFragments.put(position, fragment)
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregister when the item is inactive
|
|
||||||
override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any) {
|
|
||||||
registeredFragments.remove(position)
|
|
||||||
super.destroyItem(container, position, `object`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the fragment for the position (if instantiated)
|
|
||||||
fun getRegisteredFragment(position: Int): Fragment {
|
|
||||||
return registeredFragments.get(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRegisteredFragments(): List<Fragment> {
|
|
||||||
val fragments = ArrayList<Fragment>()
|
|
||||||
for (i in 0..registeredFragments.size() - 1) {
|
|
||||||
fragments.add(registeredFragments.valueAt(i))
|
|
||||||
}
|
|
||||||
return fragments
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,68 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.view.MenuItemCompat
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
|
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||||
|
|
||||||
|
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) {
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||||
|
val view = inflateView(inflater, container)
|
||||||
|
onViewCreated(view, savedViewState)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
|
||||||
|
|
||||||
|
open fun onViewCreated(view: View, savedViewState: Bundle?) { }
|
||||||
|
|
||||||
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
|
if (type.isEnter) {
|
||||||
|
setTitle()
|
||||||
|
}
|
||||||
|
super.onChangeStarted(handler, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getTitle(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTitle() {
|
||||||
|
var parentController = parentController
|
||||||
|
while (parentController != null) {
|
||||||
|
if (parentController is BaseController && parentController.getTitle() != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parentController = parentController.parentController
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround for disappearing menu items when collapsing an expandable item like a SearchView.
|
||||||
|
* This method should be removed when fixed upstream.
|
||||||
|
* Issue link: https://issuetracker.google.com/issues/37657375
|
||||||
|
*/
|
||||||
|
fun MenuItem.fixExpand() {
|
||||||
|
val expandListener = object : MenuItemCompat.OnActionExpandListener {
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MenuItemCompat.setOnActionExpandListener(this, expandListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
import android.os.Build
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.bluelinelabs.conductor.Router
|
||||||
|
|
||||||
|
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||||
|
val controller = getControllerWithTag(tag)
|
||||||
|
if (controller != null) {
|
||||||
|
popController(controller)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
|
||||||
|
val activity = activity ?: return
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
permissions.forEach { permission ->
|
||||||
|
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(arrayOf(permission), requestCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
|
||||||
|
import com.bluelinelabs.conductor.Router;
|
||||||
|
import com.bluelinelabs.conductor.RouterTransaction;
|
||||||
|
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A controller that displays a dialog window, floating on top of its activity's window.
|
||||||
|
* This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}.
|
||||||
|
*
|
||||||
|
* <p>Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog}
|
||||||
|
*/
|
||||||
|
public abstract class DialogController extends RestoreViewOnCreateController {
|
||||||
|
|
||||||
|
private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
|
||||||
|
|
||||||
|
private Dialog dialog;
|
||||||
|
private boolean dismissed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constructor for use when no arguments are needed.
|
||||||
|
*/
|
||||||
|
protected DialogController() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor that takes arguments that need to be retained across restarts.
|
||||||
|
*
|
||||||
|
* @param args Any arguments that need to be retained.
|
||||||
|
*/
|
||||||
|
protected DialogController(@Nullable Bundle args) {
|
||||||
|
super(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
|
||||||
|
dialog = onCreateDialog(savedViewState);
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
dialog.setOwnerActivity(getActivity());
|
||||||
|
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
dismissDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (savedViewState != null) {
|
||||||
|
Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG);
|
||||||
|
if (dialogState != null) {
|
||||||
|
dialog.onRestoreInstanceState(dialogState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new View(getActivity());//stub view
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
|
||||||
|
super.onSaveViewState(view, outState);
|
||||||
|
Bundle dialogState = dialog.onSaveInstanceState();
|
||||||
|
outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAttach(@NonNull View view) {
|
||||||
|
super.onAttach(view);
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetach(@NonNull View view) {
|
||||||
|
super.onDetach(view);
|
||||||
|
dialog.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroyView(@NonNull View view) {
|
||||||
|
super.onDestroyView(view);
|
||||||
|
dialog.setOnDismissListener(null);
|
||||||
|
dialog.dismiss();
|
||||||
|
dialog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the dialog, create a transaction and pushing the controller.
|
||||||
|
* @param router The router on which the transaction will be applied
|
||||||
|
*/
|
||||||
|
public void showDialog(@NonNull Router router) {
|
||||||
|
showDialog(router, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the dialog, create a transaction and pushing the controller.
|
||||||
|
* @param router The router on which the transaction will be applied
|
||||||
|
* @param tag The tag for this controller
|
||||||
|
*/
|
||||||
|
public void showDialog(@NonNull Router router, @Nullable String tag) {
|
||||||
|
dismissed = false;
|
||||||
|
router.pushController(RouterTransaction.with(this)
|
||||||
|
.pushChangeHandler(new SimpleSwapChangeHandler(false))
|
||||||
|
.popChangeHandler(new SimpleSwapChangeHandler(false))
|
||||||
|
.tag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the dialog and pop this controller
|
||||||
|
*/
|
||||||
|
public void dismissDialog() {
|
||||||
|
if (dismissed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getRouter().popController(this);
|
||||||
|
dismissed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected Dialog getDialog() {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build your own custom Dialog container such as an {@link android.app.AlertDialog}
|
||||||
|
*
|
||||||
|
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists.
|
||||||
|
* @return Return a new Dialog instance to be displayed by the Controller
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState);
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
interface NoToolbarElevationController
|
@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
|
||||||
|
import nucleus.factory.PresenterFactory
|
||||||
|
import nucleus.presenter.Presenter
|
||||||
|
|
||||||
|
@Suppress("LeakingThis")
|
||||||
|
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(),
|
||||||
|
PresenterFactory<P> {
|
||||||
|
|
||||||
|
private val delegate = NucleusConductorDelegate(this)
|
||||||
|
|
||||||
|
val presenter: P
|
||||||
|
get() = delegate.presenter
|
||||||
|
|
||||||
|
init {
|
||||||
|
addLifecycleListener(NucleusConductorLifecycleListener(delegate))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.view.PagerAdapter;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.Controller;
|
||||||
|
import com.bluelinelabs.conductor.Router;
|
||||||
|
import com.bluelinelabs.conductor.RouterTransaction;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter for ViewPagers that uses Routers as pages
|
||||||
|
*/
|
||||||
|
public abstract class RouterPagerAdapter extends PagerAdapter {
|
||||||
|
|
||||||
|
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
|
||||||
|
private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
|
||||||
|
private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
|
||||||
|
|
||||||
|
private final Controller host;
|
||||||
|
private int maxPagesToStateSave = Integer.MAX_VALUE;
|
||||||
|
private SparseArray<Bundle> savedPages = new SparseArray<>();
|
||||||
|
private SparseArray<Router> visibleRouters = new SparseArray<>();
|
||||||
|
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
|
||||||
|
private Router primaryRouter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RouterPagerAdapter using the passed host.
|
||||||
|
*/
|
||||||
|
public RouterPagerAdapter(@NonNull Controller host) {
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a router is instantiated. Here the router's root should be set if needed.
|
||||||
|
*
|
||||||
|
* @param router The router used for the page
|
||||||
|
* @param position The page position to be instantiated.
|
||||||
|
*/
|
||||||
|
public abstract void configureRouter(@NonNull Router router, int position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number of pages that will have their states saved. When this number is exceeded,
|
||||||
|
* the page that was state saved least recently will have its state removed from the save data.
|
||||||
|
*/
|
||||||
|
public void setMaxPagesToStateSave(int maxPagesToStateSave) {
|
||||||
|
if (maxPagesToStateSave < 0) {
|
||||||
|
throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maxPagesToStateSave = maxPagesToStateSave;
|
||||||
|
|
||||||
|
ensurePagesSaved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(ViewGroup container, int position) {
|
||||||
|
final String name = makeRouterName(container.getId(), getItemId(position));
|
||||||
|
|
||||||
|
Router router = host.getChildRouter(container, name);
|
||||||
|
if (!router.hasRootController()) {
|
||||||
|
Bundle routerSavedState = savedPages.get(position);
|
||||||
|
|
||||||
|
if (routerSavedState != null) {
|
||||||
|
router.restoreInstanceState(routerSavedState);
|
||||||
|
savedPages.remove(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.rebindIfNeeded();
|
||||||
|
configureRouter(router, position);
|
||||||
|
|
||||||
|
if (router != primaryRouter) {
|
||||||
|
for (RouterTransaction transaction : router.getBackstack()) {
|
||||||
|
transaction.controller().setOptionsMenuHidden(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleRouters.put(position, router);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||||
|
Router router = (Router)object;
|
||||||
|
|
||||||
|
Bundle savedState = new Bundle();
|
||||||
|
router.saveInstanceState(savedState);
|
||||||
|
savedPages.put(position, savedState);
|
||||||
|
|
||||||
|
savedPageHistory.remove((Integer)position);
|
||||||
|
savedPageHistory.add(position);
|
||||||
|
|
||||||
|
ensurePagesSaved();
|
||||||
|
|
||||||
|
host.removeChildRouter(router);
|
||||||
|
|
||||||
|
visibleRouters.remove(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||||
|
Router router = (Router)object;
|
||||||
|
if (router != primaryRouter) {
|
||||||
|
if (primaryRouter != null) {
|
||||||
|
for (RouterTransaction transaction : primaryRouter.getBackstack()) {
|
||||||
|
transaction.controller().setOptionsMenuHidden(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (router != null) {
|
||||||
|
for (RouterTransaction transaction : router.getBackstack()) {
|
||||||
|
transaction.controller().setOptionsMenuHidden(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryRouter = router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(View view, Object object) {
|
||||||
|
Router router = (Router)object;
|
||||||
|
final List<RouterTransaction> backstack = router.getBackstack();
|
||||||
|
for (RouterTransaction transaction : backstack) {
|
||||||
|
if (transaction.controller().getView() == view) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parcelable saveState() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
|
||||||
|
bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
|
||||||
|
bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restoreState(Parcelable state, ClassLoader loader) {
|
||||||
|
Bundle bundle = (Bundle)state;
|
||||||
|
if (state != null) {
|
||||||
|
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
|
||||||
|
maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
|
||||||
|
savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the already instantiated Router in the specified position or {@code null} if there
|
||||||
|
* is no router associated with this position.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public Router getRouter(int position) {
|
||||||
|
return visibleRouters.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
SparseArray<Bundle> getSavedPages() {
|
||||||
|
return savedPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensurePagesSaved() {
|
||||||
|
while (savedPages.size() > maxPagesToStateSave) {
|
||||||
|
int positionToRemove = savedPageHistory.remove(0);
|
||||||
|
savedPages.remove(positionToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String makeRouterName(int viewId, long id) {
|
||||||
|
return viewId + ":" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import android.view.View
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.subscriptions.CompositeSubscription
|
||||||
|
|
||||||
|
abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
|
||||||
|
|
||||||
|
var untilDetachSubscriptions = CompositeSubscription()
|
||||||
|
private set
|
||||||
|
|
||||||
|
var untilDestroySubscriptions = CompositeSubscription()
|
||||||
|
private set
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
if (untilDetachSubscriptions.isUnsubscribed) {
|
||||||
|
untilDetachSubscriptions = CompositeSubscription()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDetach(view: View) {
|
||||||
|
super.onDetach(view)
|
||||||
|
untilDetachSubscriptions.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||||
|
if (untilDestroySubscriptions.isUnsubscribed) {
|
||||||
|
untilDestroySubscriptions = CompositeSubscription()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
super.onDestroyView(view)
|
||||||
|
untilDestroySubscriptions.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
|
||||||
|
|
||||||
|
return subscribe().also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit,
|
||||||
|
onCompleted: () -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
|
||||||
|
|
||||||
|
return subscribe().also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit,
|
||||||
|
onCompleted: () -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.support.v4.widget.DrawerLayout
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
interface SecondaryDrawerController {
|
||||||
|
|
||||||
|
fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup?
|
||||||
|
|
||||||
|
fun cleanupSecondaryDrawer(drawer: DrawerLayout)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.support.design.widget.TabLayout
|
||||||
|
|
||||||
|
interface TabbedController {
|
||||||
|
|
||||||
|
fun configureTabs(tabs: TabLayout) {}
|
||||||
|
|
||||||
|
fun cleanupTabs(tabs: TabLayout) {}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
|
|
||||||
abstract class BaseFragment : Fragment(), FragmentMixin {
|
|
||||||
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import nucleus.view.NucleusSupportFragment
|
|
||||||
|
|
||||||
abstract class BaseRxFragment<P : BasePresenter<*>> : NucleusSupportFragment<P>(), FragmentMixin {
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
val superFactory = presenterFactory
|
|
||||||
setPresenterFactory {
|
|
||||||
superFactory.createPresenter().apply {
|
|
||||||
val app = activity.application as App
|
|
||||||
context = app.applicationContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onCreate(savedState)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
|
||||||
|
|
||||||
import android.support.v4.app.FragmentActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
|
||||||
|
|
||||||
interface FragmentMixin {
|
|
||||||
|
|
||||||
fun setToolbarTitle(title: String) {
|
|
||||||
(getActivity() as ActivityMixin).setToolbarTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(resourceId: Int) {
|
|
||||||
(getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getActivity(): FragmentActivity
|
|
||||||
|
|
||||||
fun getString(resource: Int): String
|
|
||||||
}
|
|
@ -1,13 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter
|
package eu.kanade.tachiyomi.ui.base.presenter
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import nucleus.presenter.RxPresenter
|
import nucleus.presenter.RxPresenter
|
||||||
import nucleus.view.ViewWithPresenter
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
|
open class BasePresenter<V> : RxPresenter<V>() {
|
||||||
|
|
||||||
lateinit var context: Context
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import nucleus.factory.PresenterFactory;
|
||||||
|
import nucleus.presenter.Presenter;
|
||||||
|
|
||||||
|
public class NucleusConductorDelegate<P extends Presenter> {
|
||||||
|
|
||||||
|
@Nullable private P presenter;
|
||||||
|
@Nullable private Bundle bundle;
|
||||||
|
|
||||||
|
private PresenterFactory<P> factory;
|
||||||
|
|
||||||
|
public NucleusConductorDelegate(PresenterFactory<P> creator) {
|
||||||
|
this.factory = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public P getPresenter() {
|
||||||
|
if (presenter == null) {
|
||||||
|
presenter = factory.createPresenter();
|
||||||
|
presenter.create(bundle);
|
||||||
|
bundle = null;
|
||||||
|
}
|
||||||
|
return presenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle onSaveInstanceState() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
getPresenter();
|
||||||
|
if (presenter != null) {
|
||||||
|
presenter.save(bundle);
|
||||||
|
}
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRestoreInstanceState(Bundle presenterState) {
|
||||||
|
bundle = presenterState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTakeView(Object view) {
|
||||||
|
getPresenter();
|
||||||
|
if (presenter != null) {
|
||||||
|
//noinspection unchecked
|
||||||
|
presenter.takeView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDropView() {
|
||||||
|
if (presenter != null) {
|
||||||
|
presenter.dropView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDestroy() {
|
||||||
|
if (presenter != null) {
|
||||||
|
presenter.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.Controller;
|
||||||
|
|
||||||
|
public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
|
||||||
|
|
||||||
|
private static final String PRESENTER_STATE_KEY = "presenter_state";
|
||||||
|
|
||||||
|
private NucleusConductorDelegate delegate;
|
||||||
|
|
||||||
|
public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
|
||||||
|
delegate.onTakeView(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
|
||||||
|
delegate.onDropView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDestroy(@NonNull Controller controller) {
|
||||||
|
delegate.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
|
||||||
|
outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
|
||||||
|
delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
|
@ -3,36 +3,45 @@ package eu.kanade.tachiyomi.ui.catalogue
|
|||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||||
|
|
||||||
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() {
|
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() {
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.item_catalogue_grid
|
return R.layout.catalogue_grid_item
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): CatalogueHolder {
|
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
parent: ViewGroup): CatalogueHolder {
|
||||||
|
|
||||||
if (parent is AutofitRecyclerView) {
|
if (parent is AutofitRecyclerView) {
|
||||||
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
|
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
|
||||||
card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4)
|
card.layoutParams = FrameLayout.LayoutParams(
|
||||||
gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
MATCH_PARENT, parent.itemWidth / 3 * 4)
|
||||||
|
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||||
|
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
||||||
}
|
}
|
||||||
return CatalogueGridHolder(view, adapter)
|
return CatalogueGridHolder(view, adapter)
|
||||||
} else {
|
} else {
|
||||||
val view = parent.inflate(R.layout.item_catalogue_list)
|
val view = parent.inflate(R.layout.catalogue_list_item)
|
||||||
return CatalogueListHolder(view, adapter)
|
return CatalogueListHolder(view, adapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: CatalogueHolder, position: Int, payloads: List<Any?>?) {
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
holder: CatalogueHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?) {
|
||||||
|
|
||||||
holder.onSetValues(manga)
|
holder.onSetValues(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
|
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
@ -42,6 +43,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
|||||||
.load(manga)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
.bitmapTransform(CropCircleTransformation(view.context))
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.placeholder(android.R.color.transparent)
|
.placeholder(android.R.color.transparent)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user