Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 263c0fae8c | |||
| 7756f25312 | |||
| 6a0b523e86 | |||
| 070e2d94c7 | |||
| 743482dfd2 | |||
| f6b7f9e29f | |||
| 5c9f98bff1 | |||
| d375d7d8c8 | |||
| a88bcb0fa2 | |||
| 5512c6eb79 | |||
| 97e4b0e248 | |||
| 99a94150ea | |||
| 26b30adf4a | |||
| 4a115785eb | |||
| a8cb77cc7e | |||
| c44c37383d | |||
| 8e72394910 | |||
| e5349a3d33 | |||
| e6aa6f02e4 | |||
| 231c75df65 | |||
| 08c2bfd263 | |||
| 33bdf011b4 | |||
| 26deb46219 | |||
| 45bfd5f72c | |||
| 32d81eb1fa | |||
| 4309b4c0d7 | |||
| 2c3f7f5206 | |||
| d670d29169 | |||
| a4c61e49f4 | |||
| 3d00e85dc2 | |||
| 46f39c24b0 | |||
| 418da04411 | |||
| 2d9cd81b62 | |||
| 2bd161d5a2 | |||
| af25e0e770 | |||
| 7cf5208000 | |||
| 12bda2a966 | |||
| 69f524717a | |||
| c6972b04d2 | |||
| 1be153e51c | |||
| 0a2684a1fe | |||
| b16f91571d | |||
| a55964ee3d | |||
| f473415968 | |||
| 78754a96d6 | |||
| 1992a2a4c4 | |||
| 897eed3ba4 | |||
| 5b2e307f92 | |||
| d21dac8a2d | |||
| 0a7933856c | |||
| 11f31769ac | |||
| f3e17edd6c | |||
| 0a110d149a | |||
| 8a1d277630 | |||
| 8244ca9898 | |||
| e98567a86b | |||
| e0c1e56588 | |||
| 3f7302c4eb | |||
| b25da34b64 | |||
| c4b67c4eb1 | |||
| 41944164e5 | |||
| a8a6effd86 | |||
| 6a45a91a50 | |||
| 9e78f4f0f1 | |||
| 88bccfc015 | |||
| ecc0082db0 | |||
| 3648ef4397 | |||
| 989119af17 | |||
| 4482ab4a68 | |||
| a700c1a230 | |||
| b487e29059 | |||
| 311b1c23e5 | |||
| 5dfc855ade | |||
| d149e3186a | |||
| c377afac2e | |||
| 9878e0025a | |||
| 28c3511984 | |||
| 7b7e625f57 | |||
| 2207d9ffa4 | |||
| ceca8207ad | |||
| c67b7092fb | |||
| 5d1d5778ad | |||
| 46bb17ce81 | |||
| 704b3b0508 | |||
| d98e0c5f68 | |||
| 30f71b126f | |||
| 0bff96e0d7 | |||
| 6ef1f566ec | |||
| 18c1234dfc | |||
| b3e8214a20 | |||
| eb533c4498 | |||
| d8179f992e | |||
| ab292d6c71 | |||
| a081b88a5b | |||
| d0e9d24f6f | |||
| 6a41d96ddf | |||
| 5d330c4f75 | |||
| 1ebcfc53d4 | |||
| 7569955f9e | |||
| f3f74264c3 | |||
| 8a32db268e | |||
| ddf9a81335 | |||
| 0ea0cd5fe3 | |||
| 75a99cbc5d | |||
| d31d99a416 | |||
| a5e691271b | |||
| 4a96b6ac77 | |||
| 09bef11e6b | |||
| 1cba2536af | |||
| e9960c0dd8 | |||
| 1ad2146d6a | |||
| 56d6964db9 | |||
| 324280aed4 | |||
| 0b2dabc7fa | |||
| 4a627ea359 | |||
| 01b8256daf | |||
| 3e27e8943b | |||
| d2972c7c5a | |||
| 7c2283c962 | |||
| 2273a50920 | |||
| 2e8393ea30 | |||
| 621c083b79 | |||
| 70b3f1bc1f | |||
| 1f8072f18b | |||
| 4b1d6400a4 | |||
| 1df1a331dd | |||
| 7918b3b26b | |||
| bf63af8137 | |||
| bc1274008d | |||
| 2026f34956 | |||
| d8c295a293 | |||
| 5460a0d563 | |||
| b46a92e613 | |||
| 1803f49732 | |||
| a7d7aa1ec5 | |||
| 8185b91f11 | |||
| 0bd09d532d | |||
| b0f5d4d1ce | |||
| b1f7165ad7 | |||
| 574dd17906 | |||
| 1231dd1496 | |||
| d343964fa7 | |||
| a64cd44d61 | |||
| 5777db5509 | |||
| 2bb9e596ba | |||
| 07e28ca5c2 | |||
| b58fb48a20 | |||
| dcd8c3a378 | |||
| 5f5dea905c | |||
| af7b0ead98 | |||
| 76b153346f | |||
| cf49b5e37a | |||
| 286844e56d | |||
| c293fd61b1 | |||
| a12758579d | |||
| 3b56bcfbba | |||
| 819d57155a | |||
| ad9f063716 | |||
| 8a55027f67 | |||
| 76e3f0e5cb | |||
| b42a0b135d | |||
| 7e3ed2f00e | |||
| 8f90aa12fb | |||
| 5bad65c027 | |||
| f133ddb14e | |||
| 026a1116ee | |||
| 0adab16fea | |||
| 01dbe7f850 | |||
| 198a59cc2d | |||
| 3ca70543d1 | |||
| 7bc436dce2 | |||
| 6b61ead0b6 | |||
| d1c40b8b85 | |||
| 75096e9808 | |||
| 1fdede99a0 | |||
| f50d23dfe6 | |||
| 255a09abf5 | |||
| 0090dfcadc | |||
| e9f175db5d | |||
| faaf0fbff3 | |||
| 2025e1bc03 | |||
| 7635373446 | |||
| 0464ec0b59 | |||
| a0af459cfc | |||
| b98dc6e1a5 | |||
| 0680e0120f | |||
| 82688f96db | |||
| 2228b24e69 | |||
| e6f7689149 | |||
| 5466832187 | |||
| 2492803741 | |||
| ee583621be | |||
| db738e727f | |||
| 60221f0fc0 | |||
| 553dfefb3a | |||
| 6cb6405e3e | |||
| 4e018828c4 | |||
| a8e3d105f1 | |||
| 2f22f56b32 | |||
| 1dd010e733 | |||
| 2eef81c468 | |||
| ffcb5f6954 | |||
| 375455d4a6 | |||
| f089991e0b | |||
| 26a8b9acc4 |
@@ -0,0 +1,5 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
indent_size=4
|
||||||
|
insert_final_newline=true
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.8.0)
|
- To the latest version of the app (stable is v1.8.3)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ body:
|
|||||||
label: Tachiyomi version
|
label: Tachiyomi version
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "1.8.0"
|
Example: "1.8.3"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.8.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.8.3](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.8.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.8.3](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ org.gradle.daemon=false
|
|||||||
org.gradle.jvmargs=-Xmx5120m
|
org.gradle.jvmargs=-Xmx5120m
|
||||||
org.gradle.workers.max=2
|
org.gradle.workers.max=2
|
||||||
|
|
||||||
kotlin.incremental=false
|
kotlin.incremental=false
|
||||||
kotlin.compiler.execution.strategy=in-process
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
name: Issue closer
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, edited, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
autoclose:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Autoclose issues
|
|
||||||
uses: arkon/issue-closer-action@v3.4
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
rules: |
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
|
||||||
"message": "The acknowledgment section was not removed."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
|
||||||
"message": "Requested information in the template was not filled out."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "both",
|
|
||||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
|
||||||
"ignoreCase": true,
|
|
||||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
name: Issue moderator
|
name: Issue moderator
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
@@ -12,3 +14,22 @@ jobs:
|
|||||||
uses: tachiyomiorg/issue-moderator-action@v1
|
uses: tachiyomiorg/issue-moderator-action@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
auto-close-rules: |
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
|
"message": "The acknowledgment section was not removed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
+16
-1
@@ -12,6 +12,21 @@ Pull requests are welcome!
|
|||||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||||
You do not need to ask for permission nor an assignment.
|
You do not need to ask for permission nor an assignment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
|
||||||
|
|
||||||
|
- Basic [Android development](https://developer.android.com/)
|
||||||
|
- [Kotlin](https://kotlinlang.org/)
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- [Android Studio](https://developer.android.com/studio)
|
||||||
|
- Emulator or phone with developer options enabled to test changes.
|
||||||
|
|
||||||
|
## Getting help
|
||||||
|
|
||||||
|
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
@@ -27,7 +42,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
|||||||
+71
-139
@@ -1,8 +1,4 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@@ -13,8 +9,8 @@ plugins {
|
|||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
apply(plugin = "com.google.gms.google-services")
|
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||||
// Firebase Crashlytics
|
// Firebase Crashlytics
|
||||||
apply(plugin = "com.google.firebase.crashlytics")
|
apply(plugin = "com.google.firebase.crashlytics")
|
||||||
}
|
}
|
||||||
@@ -29,8 +25,8 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionCode = 25
|
versionCode = 34
|
||||||
versionName = "1.8.0"
|
versionName = "1.8.3"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -120,177 +116,145 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
implementation(kotlinx.reflect)
|
||||||
|
|
||||||
val coroutinesVersion = "1.6.0"
|
implementation(kotlinx.bundles.coroutines)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation(libs.tachiyomi.api)
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.4.0-alpha01")
|
implementation(androidx.annotation)
|
||||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
implementation(androidx.appcompat)
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
|
implementation(androidx.biometricktx)
|
||||||
implementation("androidx.browser:browser:1.4.0")
|
implementation(androidx.constraintlayout)
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
implementation(androidx.coordinatorlayout)
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
implementation(androidx.corektx)
|
||||||
implementation("androidx.core:core-ktx:1.8.0-alpha02")
|
implementation(androidx.splashscreen)
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
implementation(androidx.recyclerview)
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
implementation(androidx.swiperefreshlayout)
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation(androidx.viewpager)
|
||||||
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0"
|
implementation(androidx.bundles.lifecycle)
|
||||||
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
implementation(androidx.bundles.workmanager)
|
||||||
|
|
||||||
// RX
|
// RX
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation(libs.bundles.reactivex)
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation(libs.flowreactivenetwork)
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
|
||||||
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
|
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "4.9.1"
|
implementation(libs.bundles.okhttp)
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation(libs.okio)
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okio:okio:3.0.0")
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
implementation(libs.conscrypt.android)
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf)
|
// Data serialization (JSON, protobuf)
|
||||||
val kotlinSerializationVersion = "1.3.2"
|
implementation(kotlinx.bundles.serialization)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation("app.cash.quickjs:quickjs-android:0.9.2")
|
implementation(libs.bundles.js.engine)
|
||||||
// TODO: remove Duktape once all extensions are using QuickJS
|
|
||||||
implementation("com.squareup.duktape:duktape-android:1.4.0")
|
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation("org.jsoup:jsoup:1.14.3")
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
implementation(libs.disklrucache)
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
implementation(libs.unifile)
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation(libs.junrar)
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
|
implementation(libs.bundles.sqlite)
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0-rc01")
|
implementation(libs.preferencektx)
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
implementation(libs.flowpreferences)
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
val nucleusVersion = "3.0.0"
|
implementation(libs.bundles.nucleus)
|
||||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
|
||||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
val coilVersion = "1.4.0"
|
implementation(libs.bundles.coil)
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
implementation(libs.subsamplingscaleimageview) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation(libs.natural.comparator)
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation("com.google.android.material:material:1.6.0-alpha02")
|
implementation(libs.material)
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation(libs.androidprocessbutton)
|
||||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
implementation(libs.flexible.adapter.core)
|
||||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
implementation(libs.flexible.adapter.ui)
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation(libs.viewstatepageradapter)
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation(libs.photoview)
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
implementation(libs.directionalviewpager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
implementation(libs.insetter)
|
||||||
|
implementation(libs.markwon)
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
val conductorVersion = "3.1.2"
|
implementation(libs.bundles.conductor)
|
||||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
|
||||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "1.2.0"
|
implementation(libs.bundles.flowbinding)
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("com.squareup.logcat:logcat:0.1")
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
//implementation("ch.acra:acra-http:5.8.4")
|
// implementation(libs.acra.http)
|
||||||
//"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
// "standardImplementation"(libs.firebase.analytics)
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation(libs.aboutlibraries.core)
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
val shizukuVersion = "12.1.0"
|
implementation(libs.bundles.shizuku)
|
||||||
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
|
||||||
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation(libs.junit)
|
||||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
testImplementation(libs.assertj.core)
|
||||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
testImplementation(libs.mockito.core)
|
||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
testImplementation(libs.bundles.robolectric)
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// debugImplementation(libs.leakcanary.android)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
// Changelog
|
// Changelog
|
||||||
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
implementation(sylibs.changelog)
|
||||||
|
|
||||||
// Text distance (EH)
|
// Text distance (EH)
|
||||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
implementation (sylibs.simularity)
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
implementation(sylibs.firebase.analytics)
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx:18.2.7")
|
implementation(sylibs.firebase.crashlytics.ktx)
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation("com.elvishew:xlog:1.11.0")
|
implementation(sylibs.xlog)
|
||||||
|
|
||||||
// Debug utils (EH)
|
// Debug utils (EH)
|
||||||
val debugOverlayVersion = "1.1.3"
|
debugImplementation(sylibs.debugOverlay.standard)
|
||||||
debugImplementation("com.ms-square:debugoverlay:$debugOverlayVersion")
|
"releaseTestImplementation"(sylibs.debugOverlay.noop)
|
||||||
"releaseTestImplementation"("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
releaseImplementation(sylibs.debugOverlay.noop)
|
||||||
releaseImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
testImplementation(sylibs.debugOverlay.noop)
|
||||||
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
|
||||||
|
|
||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
|
implementation(sylibs.ratingbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -321,40 +285,8 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
classpath(kotlinx.gradle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Git is needed in your system PATH for these commands to work.
|
|
||||||
// If it's not installed, you can return a random value as a workaround
|
|
||||||
fun getCommitCount(): String {
|
|
||||||
return runCommand("git rev-list --count HEAD")
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGitSha(): String {
|
|
||||||
return runCommand("git rev-parse --short HEAD")
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBuildTime(): String {
|
|
||||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
|
||||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
return df.format(Date())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runCommand(command: String): String {
|
|
||||||
val byteOut = ByteArrayOutputStream()
|
|
||||||
project.exec {
|
|
||||||
commandLine = command.split(" ")
|
|
||||||
standardOutput = byteOut
|
|
||||||
}
|
|
||||||
return String(byteOut.toByteArray()).trim()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
@@ -187,10 +186,6 @@
|
|||||||
android:name=".data.updater.AppUpdateService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".data.backup.BackupCreateService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -10,6 +11,7 @@ import android.content.IntentFilter
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.Looper
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -22,6 +24,7 @@ import coil.ImageLoader
|
|||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
|
import coil.disk.DiskCache
|
||||||
import coil.util.DebugLogger
|
import coil.util.DebugLogger
|
||||||
import com.elvishew.xlog.LogConfiguration
|
import com.elvishew.xlog.LogConfiguration
|
||||||
import com.elvishew.xlog.LogLevel
|
import com.elvishew.xlog.LogLevel
|
||||||
@@ -35,16 +38,17 @@ import com.google.firebase.analytics.ktx.analytics
|
|||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.ms_square.debugoverlay.DebugOverlay
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
@@ -77,6 +81,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
|
|
||||||
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super<Application>.onCreate()
|
super<Application>.onCreate()
|
||||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
@@ -120,7 +125,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
this@App,
|
this@App,
|
||||||
0,
|
0,
|
||||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||||
PendingIntent.FLAG_ONE_SHOT
|
PendingIntent.FLAG_ONE_SHOT,
|
||||||
)
|
)
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
}
|
}
|
||||||
@@ -139,7 +144,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
|
|
||||||
@@ -150,17 +155,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
return ImageLoader.Builder(this).apply {
|
return ImageLoader.Builder(this).apply {
|
||||||
componentRegistry {
|
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
|
||||||
|
val diskCacheInit = { CoilDiskCache.get(this@App) }
|
||||||
|
components {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
add(ImageDecoderDecoder(this@App))
|
add(ImageDecoderDecoder.Factory())
|
||||||
} else {
|
} else {
|
||||||
add(GifDecoder())
|
add(GifDecoder.Factory())
|
||||||
}
|
}
|
||||||
add(TachiyomiImageDecoder(this@App.resources))
|
add(TachiyomiImageDecoder.Factory())
|
||||||
add(ByteBufferFetcher())
|
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||||
add(MangaCoverFetcher())
|
add(MangaCoverKeyer())
|
||||||
}
|
}
|
||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
callFactory(callFactoryInit)
|
||||||
|
diskCache(diskCacheInit)
|
||||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
if (preferences.verboseLogging()) logger(DebugLogger())
|
if (preferences.verboseLogging()) logger(DebugLogger())
|
||||||
@@ -179,6 +187,27 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPackageName(): String {
|
||||||
|
// This causes freezes in Android 6/7 for some reason
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
try {
|
||||||
|
// Override the value passed as X-Requested-With in WebView requests
|
||||||
|
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||||
|
val chromiumElement = stackTrace.find {
|
||||||
|
it.className.equals(
|
||||||
|
"org.chromium.base.BuildInfo",
|
||||||
|
ignoreCase = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
|
||||||
|
return WebViewUtil.SPOOF_PACKAGE_NAME
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getPackageName()
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
try {
|
try {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
@@ -208,7 +237,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
val logFolder = File(
|
val logFolder = File(
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
getString(R.string.app_name),
|
getString(R.string.app_name),
|
||||||
"logs"
|
"logs",
|
||||||
)
|
)
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
@@ -219,7 +248,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||||
return super.generateFileName(
|
return super.generateFileName(
|
||||||
logLevel,
|
logLevel,
|
||||||
timestamp
|
timestamp,
|
||||||
) + "-${BuildConfig.BUILD_TYPE}.log"
|
) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +266,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
XLog.init(
|
XLog.init(
|
||||||
logConfig,
|
logConfig,
|
||||||
*printers.toTypedArray()
|
*printers.toTypedArray(),
|
||||||
)
|
)
|
||||||
|
|
||||||
xLogD("Application booting...")
|
xLogD("Application booting...")
|
||||||
@@ -252,7 +281,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
Device name: ${Build.DEVICE}
|
Device name: ${Build.DEVICE}
|
||||||
Device model: ${Build.MODEL}
|
Device model: ${Build.MODEL}
|
||||||
Device product name: ${Build.PRODUCT}
|
Device product name: ${Build.PRODUCT}
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,3 +325,24 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
|
||||||
|
*/
|
||||||
|
internal object CoilDiskCache {
|
||||||
|
|
||||||
|
private const val FOLDER_NAME = "image_cache"
|
||||||
|
private var instance: DiskCache? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun get(context: Context): DiskCache {
|
||||||
|
return instance ?: run {
|
||||||
|
val safeCacheDir = context.cacheDir.apply { mkdirs() }
|
||||||
|
// Create the singleton disk cache instance.
|
||||||
|
DiskCache.Builder()
|
||||||
|
.directory(safeCacheDir.resolve(FOLDER_NAME))
|
||||||
|
.build()
|
||||||
|
.also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
@@ -48,6 +49,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { DelayedTrackingStore(app) }
|
addSingletonFactory { DelayedTrackingStore(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
addSingletonFactory { CustomMangaManager(app) }
|
addSingletonFactory { CustomMangaManager(app) }
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import androidx.core.content.edit
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
@@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -244,7 +246,26 @@ object Migrations {
|
|||||||
if (oldVersion < 72) {
|
if (oldVersion < 72) {
|
||||||
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||||
if (!oldUpdateOngoingOnly) {
|
if (!oldUpdateOngoingOnly) {
|
||||||
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
|
preferences.libraryUpdateMangaRestriction() -= MANGA_NON_COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 75) {
|
||||||
|
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
|
||||||
|
if (oldSecureScreen) {
|
||||||
|
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
|
||||||
|
}
|
||||||
|
if (DeviceUtil.isMiui && preferences.extensionInstaller().get() == PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER) {
|
||||||
|
preferences.extensionInstaller().set(PreferenceValues.ExtensionInstaller.LEGACY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 76) {
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
}
|
||||||
|
if (oldVersion < 77) {
|
||||||
|
val oldReaderTap = prefs.getBoolean("reader_tap", false)
|
||||||
|
if (!oldReaderTap) {
|
||||||
|
preferences.navigationModePager().set(5)
|
||||||
|
preferences.navigationModeWebtoon().set(5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String
|
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns manga
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
|||||||
internal fun showRestoreProgress(
|
internal fun showRestoreProgress(
|
||||||
progress: Int,
|
progress: Int,
|
||||||
amount: Int,
|
amount: Int,
|
||||||
title: String
|
title: String,
|
||||||
) {
|
) {
|
||||||
notifier.showRestoreProgress(title, progress, amount)
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,22 @@ object BackupConst {
|
|||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
const val BACKUP_TYPE_FULL = 1
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
internal const val BACKUP_CUSTOM_INFO = 0x10
|
||||||
|
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
||||||
|
internal const val BACKUP_READ_MANGA = 0x20
|
||||||
|
internal const val BACKUP_READ_MANGA_MASK = 0x20
|
||||||
|
internal const val BACKUP_ALL = 0x3F
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.PowerManager
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for backing up library information to a JSON file.
|
|
||||||
*/
|
|
||||||
class BackupCreateService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
internal const val BACKUP_CUSTOM_INFO = 0x10
|
|
||||||
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
|
||||||
internal const val BACKUP_READ_MANGA = 0x20
|
|
||||||
internal const val BACKUP_READ_MANGA_MASK = 0x20
|
|
||||||
internal const val BACKUP_ALL = 0x3F
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 =
|
|
||||||
context.isServiceRunning(BackupCreateService::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a backup from library
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param flags determines what to backup
|
|
||||||
*/
|
|
||||||
fun start(context: Context, uri: Uri, flags: Int) {
|
|
||||||
if (!isRunning(context)) {
|
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake lock that will be held until the service is destroyed.
|
|
||||||
*/
|
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
|
||||||
|
|
||||||
private lateinit var notifier: BackupNotifier
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
notifier = BackupNotifier(this)
|
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
|
||||||
|
|
||||||
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService(name: Intent?): Boolean {
|
|
||||||
destroyJob()
|
|
||||||
return super.stopService(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
destroyJob()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun destroyJob() {
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
wakeLock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method needs to be implemented, but it's not used/needed.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (intent == null) return START_NOT_STICKY
|
|
||||||
|
|
||||||
try {
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
|
||||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
|
||||||
notifier.showBackupComplete(unifile)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
notifier.showBackupError(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSelf(startId)
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -20,37 +28,71 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val uri = preferences.backupsDirectory().get().toUri()
|
val notifier = BackupNotifier(context)
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
|
||||||
|
?: preferences.backupsDirectory().get().toUri()
|
||||||
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||||
|
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||||
|
|
||||||
|
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
val location = FullBackupManager(context).createBackup(uri, flags, isAutoBackup)
|
||||||
|
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
if (!isAutoBackup) notifier.showBackupError(e.message)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
|
} finally {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BackupCreator"
|
fun isManualJobRunning(context: Context): Boolean {
|
||||||
|
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
|
||||||
|
return list.find { it.state == WorkInfo.State.RUNNING } != null
|
||||||
|
}
|
||||||
|
|
||||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(),
|
interval.toLong(),
|
||||||
TimeUnit.HOURS,
|
TimeUnit.HOURS,
|
||||||
10,
|
10,
|
||||||
TimeUnit.MINUTES
|
TimeUnit.MINUTES,
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG_AUTO)
|
||||||
|
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||||
} else {
|
} else {
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
workManager.cancelUniqueWork(TAG_AUTO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startNow(context: Context, uri: Uri, flags: Int) {
|
||||||
|
val inputData = workDataOf(
|
||||||
|
IS_AUTO_BACKUP_KEY to false,
|
||||||
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
|
BACKUP_FLAGS_KEY to flags,
|
||||||
|
)
|
||||||
|
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
|
||||||
|
.addTag(TAG_MANUAL)
|
||||||
|
.setInputData(inputData)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val TAG_AUTO = "BackupCreator"
|
||||||
|
private const val TAG_MANUAL = "$TAG_AUTO:manual"
|
||||||
|
|
||||||
|
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||||
|
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||||
|
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE),
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
@@ -97,7 +97,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
context.getString(R.string.action_stop),
|
context.getString(R.string.action_stop),
|
||||||
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
|
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +124,8 @@ class BackupNotifier(private val context: Context) {
|
|||||||
R.string.restore_duration,
|
R.string.restore_duration,
|
||||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||||
TimeUnit.MILLISECONDS.toMinutes(time)
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
|
|||||||
@@ -3,19 +3,20 @@ package eu.kanade.tachiyomi.data.backup.full
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||||
@@ -38,18 +39,17 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||||
import exh.savedsearches.JsonSavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
import exh.util.executeOnIO
|
import exh.util.executeOnIO
|
||||||
import kotlinx.serialization.decodeFromString
|
import exh.util.nullIfBlank
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import java.io.FileOutputStream
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
@@ -60,9 +60,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* Create backup Json file from database
|
* Create backup Json file from database
|
||||||
*
|
*
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String {
|
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||||
// Create root object
|
// Create root object
|
||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
@@ -71,21 +71,21 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
getReadManga()
|
getReadManga()
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
} + getMergedManga() /* SY <-- */
|
} + getMergedManga() // SY <--
|
||||||
|
|
||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
backupExtensionInfo(databaseManga),
|
backupExtensionInfo(databaseManga),
|
||||||
backupSavedSearches()
|
backupSavedSearches(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var file: UniFile? = null
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
file = (
|
file = (
|
||||||
if (isJob) {
|
if (isAutoBackup) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
dir = dir.createDirectory("automatic")
|
dir = dir.createDirectory("automatic")
|
||||||
@@ -107,8 +107,19 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
)
|
)
|
||||||
?: throw Exception("Couldn't create backup file")
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
if (!file.isFile) {
|
||||||
|
throw IllegalStateException("Failed to get handle on file")
|
||||||
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
|
file.openOutputStream().also {
|
||||||
|
// Force overwrite old file
|
||||||
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
|
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
val fileUri = file.uri
|
val fileUri = file.uri
|
||||||
|
|
||||||
// Make sure it's a valid backup file
|
// Make sure it's a valid backup file
|
||||||
@@ -156,14 +167,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* @return list of [BackupSavedSearch] to be backed up
|
* @return list of [BackupSavedSearch] to be backed up
|
||||||
*/
|
*/
|
||||||
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
||||||
return preferences.savedSearches().get().mapNotNull {
|
return databaseHelper.getSavedSearches().executeAsBlocking().map {
|
||||||
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
|
||||||
BackupSavedSearch(
|
BackupSavedSearch(
|
||||||
content.name,
|
it.name,
|
||||||
content.query,
|
it.query.orEmpty(),
|
||||||
content.filters.toString(),
|
it.filtersJson ?: "[]",
|
||||||
sourceId
|
it.source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,34 +432,25 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||||
val currentSavedSearches = preferences.savedSearches().get().mapNotNull {
|
val currentSavedSearches = databaseHelper.getSavedSearches()
|
||||||
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
.executeAsBlocking()
|
||||||
val content = try {
|
|
||||||
Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
BackupSavedSearch(
|
|
||||||
content.name,
|
|
||||||
content.query,
|
|
||||||
content.filters.toString(),
|
|
||||||
sourceId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
|
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
|
||||||
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
|
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
|
||||||
}.map {
|
}.map {
|
||||||
"${it.source}:" + Json.encodeToString(
|
SavedSearch(
|
||||||
JsonSavedSearch(
|
id = null,
|
||||||
it.name,
|
it.source,
|
||||||
it.query,
|
it.name,
|
||||||
Json.decodeFromString(it.filterList)
|
it.query.nullIfBlank(),
|
||||||
)
|
filtersJson = it.filterList.nullIfBlank()
|
||||||
|
?.takeUnless { it == "[]" },
|
||||||
)
|
)
|
||||||
}.toSet()
|
}.ifEmpty { null }
|
||||||
|
|
||||||
preferences.savedSearches().set(newSavedSearches + preferences.savedSearches().get())
|
if (newSavedSearches != null) {
|
||||||
|
databaseHelper.insertSavedSearches(newSavedSearches)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ data class Backup(
|
|||||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList()
|
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class BackupCategory(
|
|||||||
// Bump by 100 to specify this is a 0.x value
|
// Bump by 100 to specify this is a 0.x value
|
||||||
@ProtoNumber(100) var flags: Int = 0,
|
@ProtoNumber(100) var flags: Int = 0,
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mangaOrder: List<Long> = emptyList()
|
@ProtoNumber(600) var mangaOrder: List<Long> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun getCategoryImpl(): CategoryImpl {
|
fun getCategoryImpl(): CategoryImpl {
|
||||||
return CategoryImpl().apply {
|
return CategoryImpl().apply {
|
||||||
@@ -30,7 +30,7 @@ class BackupCategory(
|
|||||||
name = category.name,
|
name = category.name,
|
||||||
order = category.order,
|
order = category.order,
|
||||||
flags = category.flags,
|
flags = category.flags,
|
||||||
mangaOrder = category.mangaOrder
|
mangaOrder = category.mangaOrder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ data class BackupChapter(
|
|||||||
lastPageRead = chapter.last_page_read,
|
lastPageRead = chapter.last_page_read,
|
||||||
dateFetch = chapter.date_fetch,
|
dateFetch = chapter.date_fetch,
|
||||||
dateUpload = chapter.date_upload,
|
dateUpload = chapter.date_upload,
|
||||||
sourceOrder = chapter.source_order
|
sourceOrder = chapter.source_order,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
|||||||
data class BackupFlatMetadata(
|
data class BackupFlatMetadata(
|
||||||
@ProtoNumber(1) var searchMetadata: BackupSearchMetadata,
|
@ProtoNumber(1) var searchMetadata: BackupSearchMetadata,
|
||||||
@ProtoNumber(2) var searchTags: List<BackupSearchTag> = emptyList(),
|
@ProtoNumber(2) var searchTags: List<BackupSearchTag> = emptyList(),
|
||||||
@ProtoNumber(3) var searchTitles: List<BackupSearchTitle> = emptyList()
|
@ProtoNumber(3) var searchTitles: List<BackupSearchTitle> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun getFlatMetadata(mangaId: Long): FlatMetadata {
|
fun getFlatMetadata(mangaId: Long): FlatMetadata {
|
||||||
return FlatMetadata(
|
return FlatMetadata(
|
||||||
metadata = searchMetadata.getSearchMetadata(mangaId),
|
metadata = searchMetadata.getSearchMetadata(mangaId),
|
||||||
tags = searchTags.map { it.getSearchTag(mangaId) },
|
tags = searchTags.map { it.getSearchTag(mangaId) },
|
||||||
titles = searchTitles.map { it.getSearchTitle(mangaId) }
|
titles = searchTitles.map { it.getSearchTitle(mangaId) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ data class BackupFlatMetadata(
|
|||||||
return BackupFlatMetadata(
|
return BackupFlatMetadata(
|
||||||
searchMetadata = BackupSearchMetadata.copyFrom(flatMetadata.metadata),
|
searchMetadata = BackupSearchMetadata.copyFrom(flatMetadata.metadata),
|
||||||
searchTags = flatMetadata.tags.map { BackupSearchTag.copyFrom(it) },
|
searchTags = flatMetadata.tags.map { BackupSearchTag.copyFrom(it) },
|
||||||
searchTitles = flatMetadata.titles.map { BackupSearchTitle.copyFrom(it) }
|
searchTitles = flatMetadata.titles.map { BackupSearchTitle.copyFrom(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class BrokenBackupHistory(
|
data class BrokenBackupHistory(
|
||||||
@ProtoNumber(0) var url: String,
|
@ProtoNumber(0) var url: String,
|
||||||
@ProtoNumber(1) var lastRead: Long
|
@ProtoNumber(1) var lastRead: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupHistory(
|
data class BackupHistory(
|
||||||
@ProtoNumber(1) var url: String,
|
@ProtoNumber(1) var url: String,
|
||||||
@ProtoNumber(2) var lastRead: Long
|
@ProtoNumber(2) var lastRead: Long,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ data class BackupManga(
|
|||||||
artist = customArtist,
|
artist = customArtist,
|
||||||
description = customDescription,
|
description = customDescription,
|
||||||
genre = customGenre,
|
genre = customGenre,
|
||||||
status = customStatus.takeUnless { it == 0 }
|
status = customStatus.takeUnless { it == 0 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -127,7 +127,7 @@ data class BackupManga(
|
|||||||
viewer = manga.readingModeType,
|
viewer = manga.readingModeType,
|
||||||
viewer_flags = manga.viewer_flags,
|
viewer_flags = manga.viewer_flags,
|
||||||
chapterFlags = manga.chapter_flags,
|
chapterFlags = manga.chapter_flags,
|
||||||
filtered_scanlators = manga.filtered_scanlators
|
filtered_scanlators = manga.filtered_scanlators,
|
||||||
// SY -->
|
// SY -->
|
||||||
).also { backupManga ->
|
).also { backupManga ->
|
||||||
customMangaManager?.getManga(manga)?.let {
|
customMangaManager?.getManga(manga)?.let {
|
||||||
|
|||||||
+3
-3
@@ -16,7 +16,7 @@ data class BackupMergedMangaReference(
|
|||||||
@ProtoNumber(5) var downloadChapters: Boolean,
|
@ProtoNumber(5) var downloadChapters: Boolean,
|
||||||
@ProtoNumber(6) var mergeUrl: String,
|
@ProtoNumber(6) var mergeUrl: String,
|
||||||
@ProtoNumber(7) var mangaUrl: String,
|
@ProtoNumber(7) var mangaUrl: String,
|
||||||
@ProtoNumber(8) var mangaSourceId: Long
|
@ProtoNumber(8) var mangaSourceId: Long,
|
||||||
) {
|
) {
|
||||||
fun getMergedMangaReference(): MergedMangaReference {
|
fun getMergedMangaReference(): MergedMangaReference {
|
||||||
return MergedMangaReference(
|
return MergedMangaReference(
|
||||||
@@ -30,7 +30,7 @@ data class BackupMergedMangaReference(
|
|||||||
mangaSourceId = mangaSourceId,
|
mangaSourceId = mangaSourceId,
|
||||||
mergeId = null,
|
mergeId = null,
|
||||||
mangaId = null,
|
mangaId = null,
|
||||||
id = null
|
id = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ data class BackupMergedMangaReference(
|
|||||||
downloadChapters = mergedMangaReference.downloadChapters,
|
downloadChapters = mergedMangaReference.downloadChapters,
|
||||||
mergeUrl = mergedMangaReference.mergeUrl,
|
mergeUrl = mergedMangaReference.mergeUrl,
|
||||||
mangaUrl = mergedMangaReference.mangaUrl,
|
mangaUrl = mergedMangaReference.mangaUrl,
|
||||||
mangaSourceId = mergedMangaReference.mangaSourceId
|
mangaSourceId = mergedMangaReference.mangaSourceId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ data class BackupSavedSearch(
|
|||||||
@ProtoNumber(1) val name: String,
|
@ProtoNumber(1) val name: String,
|
||||||
@ProtoNumber(2) val query: String = "",
|
@ProtoNumber(2) val query: String = "",
|
||||||
@ProtoNumber(3) val filterList: String = "",
|
@ProtoNumber(3) val filterList: String = "",
|
||||||
@ProtoNumber(4) val source: Long = 0
|
@ProtoNumber(4) val source: Long = 0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class BrokenBackupSource(
|
data class BrokenBackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ProtoNumber(1) var sourceId: Long
|
@ProtoNumber(1) var sourceId: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupSource(
|
data class BackupSource(
|
||||||
@ProtoNumber(1) var name: String = "",
|
@ProtoNumber(1) var name: String = "",
|
||||||
@ProtoNumber(2) var sourceId: Long
|
@ProtoNumber(2) var sourceId: Long,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(source: Source): BackupSource {
|
fun copyFrom(source: Source): BackupSource {
|
||||||
return BackupSource(
|
return BackupSource(
|
||||||
name = source.name,
|
name = source.name,
|
||||||
sourceId = source.id
|
sourceId = source.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ data class BackupTracking(
|
|||||||
status = track.status,
|
status = track.status,
|
||||||
startedReadingDate = track.started_reading_date,
|
startedReadingDate = track.started_reading_date,
|
||||||
finishedReadingDate = track.finished_reading_date,
|
finishedReadingDate = track.finished_reading_date,
|
||||||
trackingUrl = track.tracking_url
|
trackingUrl = track.tracking_url,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ data class BackupSearchMetadata(
|
|||||||
@ProtoNumber(1) var uploader: String? = null,
|
@ProtoNumber(1) var uploader: String? = null,
|
||||||
@ProtoNumber(2) var extra: String,
|
@ProtoNumber(2) var extra: String,
|
||||||
@ProtoNumber(3) var indexedExtra: String? = null,
|
@ProtoNumber(3) var indexedExtra: String? = null,
|
||||||
@ProtoNumber(4) var extraVersion: Int
|
@ProtoNumber(4) var extraVersion: Int,
|
||||||
) {
|
) {
|
||||||
fun getSearchMetadata(mangaId: Long): SearchMetadata {
|
fun getSearchMetadata(mangaId: Long): SearchMetadata {
|
||||||
return SearchMetadata(
|
return SearchMetadata(
|
||||||
@@ -17,7 +17,7 @@ data class BackupSearchMetadata(
|
|||||||
uploader = uploader,
|
uploader = uploader,
|
||||||
extra = extra,
|
extra = extra,
|
||||||
indexedExtra = indexedExtra,
|
indexedExtra = indexedExtra,
|
||||||
extraVersion = extraVersion
|
extraVersion = extraVersion,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ data class BackupSearchMetadata(
|
|||||||
uploader = searchMetadata.uploader,
|
uploader = searchMetadata.uploader,
|
||||||
extra = searchMetadata.extra,
|
extra = searchMetadata.extra,
|
||||||
indexedExtra = searchMetadata.indexedExtra,
|
indexedExtra = searchMetadata.indexedExtra,
|
||||||
extraVersion = searchMetadata.extraVersion
|
extraVersion = searchMetadata.extraVersion,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -8,7 +8,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
|||||||
data class BackupSearchTag(
|
data class BackupSearchTag(
|
||||||
@ProtoNumber(1) var namespace: String? = null,
|
@ProtoNumber(1) var namespace: String? = null,
|
||||||
@ProtoNumber(2) var name: String,
|
@ProtoNumber(2) var name: String,
|
||||||
@ProtoNumber(3) var type: Int
|
@ProtoNumber(3) var type: Int,
|
||||||
) {
|
) {
|
||||||
fun getSearchTag(mangaId: Long): SearchTag {
|
fun getSearchTag(mangaId: Long): SearchTag {
|
||||||
return SearchTag(
|
return SearchTag(
|
||||||
@@ -16,7 +16,7 @@ data class BackupSearchTag(
|
|||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
namespace = namespace,
|
namespace = namespace,
|
||||||
name = name,
|
name = name,
|
||||||
type = type
|
type = type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ data class BackupSearchTag(
|
|||||||
return BackupSearchTag(
|
return BackupSearchTag(
|
||||||
namespace = searchTag.namespace,
|
namespace = searchTag.namespace,
|
||||||
name = searchTag.name,
|
name = searchTag.name,
|
||||||
type = searchTag.type
|
type = searchTag.type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -7,14 +7,14 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class BackupSearchTitle(
|
data class BackupSearchTitle(
|
||||||
@ProtoNumber(1) var title: String,
|
@ProtoNumber(1) var title: String,
|
||||||
@ProtoNumber(2) var type: Int
|
@ProtoNumber(2) var type: Int,
|
||||||
) {
|
) {
|
||||||
fun getSearchTitle(mangaId: Long): SearchTitle {
|
fun getSearchTitle(mangaId: Long): SearchTitle {
|
||||||
return SearchTitle(
|
return SearchTitle(
|
||||||
id = null,
|
id = null,
|
||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
title = title,
|
title = title,
|
||||||
type = type
|
type = type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ data class BackupSearchTitle(
|
|||||||
fun copyFrom(searchTitle: SearchTitle): BackupSearchTitle {
|
fun copyFrom(searchTitle: SearchTitle): BackupSearchTitle {
|
||||||
return BackupSearchTitle(
|
return BackupSearchTitle(
|
||||||
title = searchTitle.title,
|
title = searchTitle.title,
|
||||||
type = searchTitle.type
|
type = searchTitle.type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,16 @@ import eu.kanade.tachiyomi.source.model.toSManga
|
|||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.savedsearches.JsonSavedSearch
|
import exh.savedsearches.models.SavedSearch
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
|
import exh.util.nullIfBlank
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.contextual
|
import kotlinx.serialization.modules.contextual
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -67,9 +72,9 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
* Create backup Json file from database
|
* Create backup Json file from database
|
||||||
*
|
*
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean) =
|
||||||
throw IllegalStateException("Legacy backup creation is not supported")
|
throw IllegalStateException("Legacy backup creation is not supported")
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
@@ -287,34 +292,26 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
internal fun restoreSavedSearches(jsonSavedSearches: String) {
|
internal fun restoreSavedSearches(jsonSavedSearches: String) {
|
||||||
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
|
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
|
||||||
|
|
||||||
|
val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking()
|
||||||
|
|
||||||
val newSavedSearches = backupSavedSearches.mapNotNull {
|
val newSavedSearches = backupSavedSearches.mapNotNull {
|
||||||
runCatching {
|
runCatching {
|
||||||
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
|
||||||
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
SavedSearch(
|
||||||
id to content
|
id = null,
|
||||||
|
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
|
||||||
|
content["name"]!!.jsonPrimitive.content,
|
||||||
|
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
|
||||||
|
Json.encodeToString(content["filters"]!!.jsonArray),
|
||||||
|
)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}.toMutableSet()
|
}.filter { backupSavedSearch ->
|
||||||
|
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
|
||||||
|
}.ifEmpty { null }
|
||||||
|
|
||||||
val currentSources = newSavedSearches.map(Pair<Long, *>::first).toSet()
|
if (newSavedSearches != null) {
|
||||||
|
databaseHelper.insertSavedSearches(newSavedSearches)
|
||||||
newSavedSearches += preferences.savedSearches().get().mapNotNull {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
|
||||||
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
|
||||||
id to content
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val otherSerialized = preferences.savedSearches().get().mapNotNull {
|
|
||||||
val sourceId = it.substringBefore(":").toLongOrNull() ?: return@mapNotNull null
|
|
||||||
if (sourceId in currentSources) return@mapNotNull null
|
|
||||||
it
|
|
||||||
}.toSet()
|
|
||||||
|
|
||||||
val newSerialized = newSavedSearches.map { (source, savedSearch) ->
|
|
||||||
"$source:" + Json.encodeToString(savedSearch)
|
|
||||||
}.toSet()
|
|
||||||
preferences.savedSearches().set(otherSerialized + newSerialized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
// Read the json and create a Json Object,
|
// Read the json and create a Json Object,
|
||||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||||
val backupObject = Json.decodeFromStream<JsonObject>(
|
val backupObject = Json.decodeFromStream<JsonObject>(
|
||||||
context.contentResolver.openInputStream(uri)!!
|
context.contentResolver.openInputStream(uri)!!,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get parser version
|
// Get parser version
|
||||||
@@ -143,7 +143,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>,
|
||||||
) {
|
) {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||||
@@ -195,7 +195,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>,
|
||||||
) {
|
) {
|
||||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||||
updateChapters(source, backupManga, chapters)
|
updateChapters(source, backupManga, chapters)
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
|
|
||||||
val backup = try {
|
val backup = try {
|
||||||
backupManager.parser.decodeFromStream<Backup>(
|
backupManager.parser.decodeFromStream<Backup>(
|
||||||
context.contentResolver.openInputStream(uri)!!
|
context.contentResolver.openInputStream(uri)!!,
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw ValidatorParseException(e)
|
throw ValidatorParseException(e)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ data class Backup(
|
|||||||
// SY Specific values
|
// SY Specific values
|
||||||
@SerialName("mergedmangareferences")
|
@SerialName("mergedmangareferences")
|
||||||
var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null,
|
var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null,
|
||||||
var savedSearches: String? = null
|
var savedSearches: String? = null,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val CURRENT_VERSION = 2
|
const val CURRENT_VERSION = 2
|
||||||
@@ -39,5 +39,5 @@ data class MangaObject(
|
|||||||
var chapters: List<@Contextual Chapter>? = null,
|
var chapters: List<@Contextual Chapter>? = null,
|
||||||
var categories: List<String>? = null,
|
var categories: List<String>? = null,
|
||||||
var track: List<@Contextual Track>? = null,
|
var track: List<@Contextual Track>? = null,
|
||||||
var history: List<@Contextual DHistory>? = null
|
var history: List<@Contextual DHistory>? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
|||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
add(value.name)
|
add(value.name)
|
||||||
add(value.order)
|
add(value.order)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
|||||||
if (value.last_page_read != 0) {
|
if (value.last_page_read != 0) {
|
||||||
put(LAST_READ, value.last_page_read)
|
put(LAST_READ, value.last_page_read)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
|
|||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
add(value.url)
|
add(value.url)
|
||||||
add(value.lastRead)
|
add(value.lastRead)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
|
|||||||
val array = decoder.decodeJsonElement().jsonArray
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
return DHistory(
|
return DHistory(
|
||||||
url = array[0].jsonPrimitive.content,
|
url = array[0].jsonPrimitive.content,
|
||||||
lastRead = array[1].jsonPrimitive.long
|
lastRead = array[1].jsonPrimitive.long,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
|||||||
add(value.source)
|
add(value.source)
|
||||||
add(value.viewer_flags)
|
add(value.viewer_flags)
|
||||||
add(value.chapter_flags)
|
add(value.chapter_flags)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@ object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
|
|||||||
add(value.getChapterUpdates)
|
add(value.getChapterUpdates)
|
||||||
add(value.isInfoManga)
|
add(value.isInfoManga)
|
||||||
add(value.downloadChapters)
|
add(value.downloadChapters)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
|
|||||||
isInfoManga = array[6].jsonPrimitive.boolean,
|
isInfoManga = array[6].jsonPrimitive.boolean,
|
||||||
downloadChapters = array[7].jsonPrimitive.boolean,
|
downloadChapters = array[7].jsonPrimitive.boolean,
|
||||||
mangaId = null,
|
mangaId = null,
|
||||||
mergeId = null
|
mergeId = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
|||||||
put(LIBRARY, value.library_id)
|
put(LIBRARY, value.library_id)
|
||||||
put(LAST_READ, value.last_chapter_read)
|
put(LAST_READ, value.last_chapter_read)
|
||||||
put(TRACKING_URL, value.tracking_url)
|
put(TRACKING_URL, value.tracking_url)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_VALUE_COUNT,
|
||||||
cacheSize * 1024 * 1024
|
cacheSize * 1024 * 1024,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// <-- EH
|
// <-- EH
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
|
|||||||
* Clear coil's memory cache.
|
* Clear coil's memory cache.
|
||||||
*/
|
*/
|
||||||
fun clearMemoryCache() {
|
fun clearMemoryCache() {
|
||||||
context.imageLoader.memoryCache.clear()
|
context.imageLoader.memoryCache?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCacheDir(dir: String): File {
|
private fun getCacheDir(dir: String): File {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.size.Size
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
|
||||||
return SourceResult(
|
|
||||||
source = ByteArrayInputStream(data.array()).source().buffer(),
|
|
||||||
mimeType = null,
|
|
||||||
dataSource = DataSource.MEMORY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun key(data: ByteBuffer): String? = null
|
|
||||||
}
|
|
||||||
@@ -1,149 +1,261 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.Options
|
import coil.decode.ImageSource
|
||||||
|
import coil.disk.DiskCache
|
||||||
import coil.fetch.FetchResult
|
import coil.fetch.FetchResult
|
||||||
import coil.fetch.Fetcher
|
import coil.fetch.Fetcher
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
import coil.network.HttpException
|
import coil.network.HttpException
|
||||||
import coil.request.get
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.request.Parameters
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.internal.closeQuietly
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
* A [Fetcher] that fetches cover image for [Manga] object.
|
||||||
|
*
|
||||||
|
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
|
||||||
|
* Disk caching for library items is handled by [CoverCache], otherwise
|
||||||
|
* handled by Coil's [DiskCache].
|
||||||
*
|
*
|
||||||
* Available request parameter:
|
* Available request parameter:
|
||||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||||
*/
|
*/
|
||||||
class MangaCoverFetcher : Fetcher<Manga> {
|
class MangaCoverFetcher(
|
||||||
private val coverCache: CoverCache by injectLazy()
|
private val manga: Manga,
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceLazy: Lazy<HttpSource?>,
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
private val options: Options,
|
||||||
|
private val coverCache: CoverCache,
|
||||||
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
|
private val diskCacheLazy: Lazy<DiskCache>,
|
||||||
|
) : Fetcher {
|
||||||
|
|
||||||
override fun key(data: Manga): String? {
|
// For non-custom cover
|
||||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
|
||||||
return data.thumbnail_url!!
|
private lateinit var url: String
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
// Use custom cover if exists
|
// Use custom cover if exists
|
||||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
|
||||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
val customCoverFile = coverCache.getCustomCoverFile(manga)
|
||||||
if (useCustomCover && customCoverFile.exists()) {
|
if (useCustomCover && customCoverFile.exists()) {
|
||||||
return fileLoader(customCoverFile)
|
return fileLoader(customCoverFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cover = data.thumbnail_url
|
// diskCacheKey is thumbnail_url
|
||||||
return when (getResourceType(cover)) {
|
url = diskCacheKey ?: error("No cover specified")
|
||||||
Type.URL -> httpLoader(data, options)
|
return when (getResourceType(url)) {
|
||||||
Type.File -> fileLoader(data)
|
Type.URL -> httpLoader()
|
||||||
|
Type.File -> fileLoader(File(url.substringAfter("file://")))
|
||||||
null -> error("Invalid image")
|
null -> error("Invalid image")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
private fun fileLoader(file: File): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun httpLoader(): FetchResult {
|
||||||
// Only cache separately if it's a library item
|
// Only cache separately if it's a library item
|
||||||
val coverCacheFile = if (manga.favorite) {
|
val libraryCoverCacheFile = if (manga.favorite) {
|
||||||
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||||
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
return fileLoader(libraryCoverCacheFile)
|
||||||
return fileLoader(coverCacheFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
var snapshot = readFromDiskCache()
|
||||||
if (!response.isSuccessful) {
|
try {
|
||||||
body.close()
|
// Fetch from disk cache
|
||||||
|
if (snapshot != null) {
|
||||||
|
val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, libraryCoverCacheFile)
|
||||||
|
if (snapshotCoverCache != null) {
|
||||||
|
// Read from cover cache after added to library
|
||||||
|
return fileLoader(snapshotCoverCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from snapshot
|
||||||
|
return SourceResult(
|
||||||
|
source = snapshot.toImageSource(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from network
|
||||||
|
val response = executeNetworkRequest()
|
||||||
|
val responseBody = checkNotNull(response.body) { "Null response source" }
|
||||||
|
try {
|
||||||
|
// Read from cover cache after library manga cover updated
|
||||||
|
val responseCoverCache = writeResponseToCoverCache(response, libraryCoverCacheFile)
|
||||||
|
if (responseCoverCache != null) {
|
||||||
|
return fileLoader(responseCoverCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from disk cache
|
||||||
|
snapshot = writeToDiskCache(snapshot, response)
|
||||||
|
if (snapshot != null) {
|
||||||
|
return SourceResult(
|
||||||
|
source = snapshot.toImageSource(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from response if cache is unused or unusable
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
responseBody.closeQuietly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snapshot?.closeQuietly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeNetworkRequest(): Response {
|
||||||
|
val client = sourceLazy.value?.client ?: callFactoryLazy.value
|
||||||
|
val response = client.newCall(newRequest()).await()
|
||||||
|
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
|
response.body?.closeQuietly()
|
||||||
throw HttpException(response)
|
throw HttpException(response)
|
||||||
}
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
private fun newRequest(): Request {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
val request = Request.Builder()
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
.url(url)
|
||||||
coverCacheFile.parentFile?.mkdirs()
|
.headers(sourceLazy.value?.headers ?: options.headers)
|
||||||
if (coverCacheFile.exists()) {
|
// Support attaching custom data to the network request.
|
||||||
coverCacheFile.delete()
|
.tag(Parameters::class.java, options.parameters)
|
||||||
}
|
|
||||||
coverCacheFile.sink().buffer().use { output ->
|
val diskRead = options.diskCachePolicy.readEnabled
|
||||||
output.writeAll(input)
|
val networkRead = options.networkCachePolicy.readEnabled
|
||||||
}
|
when {
|
||||||
|
!networkRead && diskRead -> {
|
||||||
|
request.cacheControl(CacheControl.FORCE_CACHE)
|
||||||
|
}
|
||||||
|
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
request.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
|
} else {
|
||||||
|
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||||
|
}
|
||||||
|
!networkRead && !diskRead -> {
|
||||||
|
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||||
|
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SourceResult(
|
return request.build()
|
||||||
source = body.source(),
|
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
|
||||||
val call = getCall(manga, options)
|
if (cacheFile == null) return null
|
||||||
val response = call.await()
|
return try {
|
||||||
return response to checkNotNull(response.body) { "Null response source" }
|
diskCacheLazy.value.run {
|
||||||
}
|
fileSystem.source(snapshot.data).use { input ->
|
||||||
|
writeSourceToCoverCache(input, cacheFile)
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
}
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
remove(diskCacheKey!!)
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
|
||||||
if (source != null) {
|
|
||||||
it.headers(source.headers)
|
|
||||||
}
|
}
|
||||||
|
cacheFile.takeIf { it.exists() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to write snapshot data to cover cache ${cacheFile.name}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val networkRead = options.networkCachePolicy.readEnabled
|
private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
|
||||||
val diskRead = options.diskCachePolicy.readEnabled
|
if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
|
||||||
when {
|
return try {
|
||||||
!networkRead && diskRead -> {
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
writeSourceToCoverCache(input, cacheFile)
|
||||||
}
|
|
||||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
|
||||||
it.cacheControl(CacheControl.FORCE_NETWORK)
|
|
||||||
} else {
|
|
||||||
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
!networkRead && !diskRead -> {
|
|
||||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
|
||||||
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.build()
|
cacheFile.takeIf { it.exists() }
|
||||||
|
} catch (e: Exception) {
|
||||||
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
logcat(LogPriority.ERROR, e) { "Failed to write response data to cover cache ${cacheFile.name}" }
|
||||||
return client.newCall(request)
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
|
||||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
cacheFile.parentFile?.mkdirs()
|
||||||
|
cacheFile.delete()
|
||||||
|
try {
|
||||||
|
cacheFile.sink().buffer().use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
cacheFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(file: File): FetchResult {
|
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||||
return SourceResult(
|
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
|
||||||
source = file.source().buffer(),
|
}
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = DataSource.DISK
|
private fun writeToDiskCache(
|
||||||
)
|
snapshot: DiskCache.Snapshot?,
|
||||||
|
response: Response,
|
||||||
|
): DiskCache.Snapshot? {
|
||||||
|
if (!options.diskCachePolicy.writeEnabled) {
|
||||||
|
snapshot?.closeQuietly()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val editor = if (snapshot != null) {
|
||||||
|
snapshot.closeAndEdit()
|
||||||
|
} else {
|
||||||
|
diskCacheLazy.value.edit(diskCacheKey!!)
|
||||||
|
} ?: return null
|
||||||
|
try {
|
||||||
|
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||||
|
response.body!!.source().readAll(this)
|
||||||
|
}
|
||||||
|
return editor.commitAndGet()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
editor.abort()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||||
|
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getResourceType(cover: String?): Type? {
|
private fun getResourceType(cover: String?): Type? {
|
||||||
@@ -159,6 +271,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
File, URL
|
File, URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
|
private val diskCacheLazy: Lazy<DiskCache>,
|
||||||
|
) : Fetcher.Factory<Manga> {
|
||||||
|
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||||
|
val source = lazy { sourceManager.get(data.source) as? HttpSource }
|
||||||
|
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.key.Keyer
|
||||||
|
import coil.request.Options
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
class MangaCoverKeyer : Keyer<Manga> {
|
||||||
|
override fun key(data: Manga, options: Options): String? {
|
||||||
|
return data.thumbnail_url?.takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import coil.bitmap.BitmapPool
|
import coil.ImageLoader
|
||||||
import coil.decode.DecodeResult
|
import coil.decode.DecodeResult
|
||||||
import coil.decode.Decoder
|
import coil.decode.Decoder
|
||||||
import coil.decode.Options
|
import coil.decode.ImageDecoderDecoder
|
||||||
import coil.size.Size
|
import coil.decode.ImageSource
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.request.Options
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
@@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
|
|||||||
/**
|
/**
|
||||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
*/
|
*/
|
||||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||||
|
|
||||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
override suspend fun decode(): DecodeResult {
|
||||||
val type = source.peek().inputStream().use {
|
val decoder = resources.sourceOrNull()?.use {
|
||||||
ImageUtil.findImageType(it)
|
|
||||||
}
|
|
||||||
return when (type) {
|
|
||||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
|
||||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun decode(
|
|
||||||
pool: BitmapPool,
|
|
||||||
source: BufferedSource,
|
|
||||||
size: Size,
|
|
||||||
options: Options
|
|
||||||
): DecodeResult {
|
|
||||||
val decoder = source.use {
|
|
||||||
ImageDecoder.newInstance(it.inputStream())
|
ImageDecoder.newInstance(it.inputStream())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
|||||||
check(bitmap != null) { "Failed to decode image." }
|
check(bitmap != null) { "Failed to decode image." }
|
||||||
|
|
||||||
return DecodeResult(
|
return DecodeResult(
|
||||||
drawable = bitmap.toDrawable(resources),
|
drawable = bitmap.toDrawable(options.context.resources),
|
||||||
isSampled = false
|
isSampled = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory : Decoder.Factory {
|
||||||
|
|
||||||
|
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||||
|
if (!isApplicable(result.source.source())) return null
|
||||||
|
return TachiyomiImageDecoder(result.source, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isApplicable(source: BufferedSource): Boolean {
|
||||||
|
val type = source.peek().inputStream().use {
|
||||||
|
ImageUtil.findImageType(it)
|
||||||
|
}
|
||||||
|
return when (type) {
|
||||||
|
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||||
|
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
|
||||||
|
|
||||||
|
override fun hashCode() = javaClass.hashCode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,33 @@ import exh.metadata.sql.models.SearchTitle
|
|||||||
import exh.metadata.sql.queries.SearchMetadataQueries
|
import exh.metadata.sql.queries.SearchMetadataQueries
|
||||||
import exh.metadata.sql.queries.SearchTagQueries
|
import exh.metadata.sql.queries.SearchTagQueries
|
||||||
import exh.metadata.sql.queries.SearchTitleQueries
|
import exh.metadata.sql.queries.SearchTitleQueries
|
||||||
|
import exh.savedsearches.mappers.FeedSavedSearchTypeMapping
|
||||||
|
import exh.savedsearches.mappers.SavedSearchTypeMapping
|
||||||
|
import exh.savedsearches.models.FeedSavedSearch
|
||||||
|
import exh.savedsearches.models.SavedSearch
|
||||||
|
import exh.savedsearches.queries.FeedSavedSearchQueries
|
||||||
|
import exh.savedsearches.queries.SavedSearchQueries
|
||||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries /* SY <-- */ {
|
MangaQueries,
|
||||||
|
ChapterQueries,
|
||||||
|
TrackQueries,
|
||||||
|
CategoryQueries,
|
||||||
|
MangaCategoryQueries,
|
||||||
|
HistoryQueries,
|
||||||
|
/* SY --> */
|
||||||
|
SearchMetadataQueries,
|
||||||
|
SearchTagQueries,
|
||||||
|
SearchTitleQueries,
|
||||||
|
MergedQueries,
|
||||||
|
FavoriteEntryQueries,
|
||||||
|
SavedSearchQueries,
|
||||||
|
FeedSavedSearchQueries
|
||||||
|
/* SY <-- */ {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@@ -63,6 +83,8 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
|
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
|
||||||
|
.addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping())
|
||||||
|
.addTypeMapping(FeedSavedSearch::class.java, FeedSavedSearchTypeMapping())
|
||||||
// SY <--
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import exh.merged.sql.tables.MergedTable
|
|||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
import exh.metadata.sql.tables.SearchTitleTable
|
import exh.metadata.sql.tables.SearchTitleTable
|
||||||
|
import exh.savedsearches.tables.FeedSavedSearchTable
|
||||||
|
import exh.savedsearches.tables.SavedSearchTable
|
||||||
|
|
||||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = /* SY --> */ 12 /* SY <-- */
|
const val DATABASE_VERSION = /* SY --> */ 13 // SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -41,6 +43,8 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
execSQL(FavoriteEntryTable.createTableQuery)
|
execSQL(FavoriteEntryTable.createTableQuery)
|
||||||
|
execSQL(SavedSearchTable.createTableQuery)
|
||||||
|
execSQL(FeedSavedSearchTable.createTableQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
@@ -57,6 +61,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
execSQL(MergedTable.createIndexQuery)
|
execSQL(MergedTable.createIndexQuery)
|
||||||
|
execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +106,11 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
if (oldVersion < 12) {
|
if (oldVersion < 12) {
|
||||||
db.execSQL(FavoriteEntryTable.fixTableQuery)
|
db.execSQL(FavoriteEntryTable.fixTableQuery)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 13) {
|
||||||
|
db.execSQL(SavedSearchTable.createTableQuery)
|
||||||
|
db.execSQL(FeedSavedSearchTable.createTableQuery)
|
||||||
|
db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
|||||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||||
CategoryPutResolver(),
|
CategoryPutResolver(),
|
||||||
CategoryGetResolver(),
|
CategoryGetResolver(),
|
||||||
CategoryDeleteResolver()
|
CategoryDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||||
@@ -42,7 +42,7 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
COL_NAME to obj.name,
|
COL_NAME to obj.name,
|
||||||
COL_ORDER to obj.order,
|
COL_ORDER to obj.order,
|
||||||
COL_FLAGS to obj.flags,
|
COL_FLAGS to obj.flags,
|
||||||
COL_MANGA_ORDER to obj.mangaOrder.joinToString("/")
|
COL_MANGA_ORDER to obj.mangaOrder.joinToString("/"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
|||||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||||
ChapterPutResolver(),
|
ChapterPutResolver(),
|
||||||
ChapterGetResolver(),
|
ChapterGetResolver(),
|
||||||
ChapterDeleteResolver()
|
ChapterDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||||
@@ -56,7 +56,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
COL_DATE_UPLOAD to obj.date_upload,
|
COL_DATE_UPLOAD to obj.date_upload,
|
||||||
COL_LAST_PAGE_READ to obj.last_page_read,
|
COL_LAST_PAGE_READ to obj.last_page_read,
|
||||||
COL_CHAPTER_NUMBER to obj.chapter_number,
|
COL_CHAPTER_NUMBER to obj.chapter_number,
|
||||||
COL_SOURCE_ORDER to obj.source_order
|
COL_SOURCE_ORDER to obj.source_order,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
|||||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||||
HistoryPutResolver(),
|
HistoryPutResolver(),
|
||||||
HistoryGetResolver(),
|
HistoryGetResolver(),
|
||||||
HistoryDeleteResolver()
|
HistoryDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||||
@@ -40,7 +40,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
COL_ID to obj.id,
|
COL_ID to obj.id,
|
||||||
COL_CHAPTER_ID to obj.chapter_id,
|
COL_CHAPTER_ID to obj.chapter_id,
|
||||||
COL_LAST_READ to obj.last_read,
|
COL_LAST_READ to obj.last_read,
|
||||||
COL_TIME_READ to obj.time_read
|
COL_TIME_READ to obj.time_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
|||||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||||
MangaCategoryPutResolver(),
|
MangaCategoryPutResolver(),
|
||||||
MangaCategoryGetResolver(),
|
MangaCategoryGetResolver(),
|
||||||
MangaCategoryDeleteResolver()
|
MangaCategoryDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||||
@@ -37,7 +37,7 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
COL_ID to obj.id,
|
COL_ID to obj.id,
|
||||||
COL_MANGA_ID to obj.manga_id,
|
COL_MANGA_ID to obj.manga_id,
|
||||||
COL_CATEGORY_ID to obj.category_id
|
COL_CATEGORY_ID to obj.category_id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
|||||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||||
MangaPutResolver(),
|
MangaPutResolver(),
|
||||||
MangaGetResolver(),
|
MangaGetResolver(),
|
||||||
MangaDeleteResolver()
|
MangaDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||||
@@ -70,7 +70,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||||
COL_DATE_ADDED to obj.date_added,
|
COL_DATE_ADDED to obj.date_added,
|
||||||
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
|
COL_FILTERED_SCANLATORS to obj.filtered_scanlators,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
|||||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
TrackPutResolver(),
|
TrackPutResolver(),
|
||||||
TrackGetResolver(),
|
TrackGetResolver(),
|
||||||
TrackDeleteResolver()
|
TrackDeleteResolver(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||||
@@ -58,7 +58,7 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
COL_TRACKING_URL to obj.tracking_url,
|
COL_TRACKING_URL to obj.tracking_url,
|
||||||
COL_SCORE to obj.score,
|
COL_SCORE to obj.score,
|
||||||
COL_START_DATE to obj.started_reading_date,
|
COL_START_DATE to obj.started_reading_date,
|
||||||
COL_FINISH_DATE to obj.finished_reading_date
|
COL_FINISH_DATE to obj.finished_reading_date,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
|
|
||||||
class LibraryManga : MangaImpl() {
|
class LibraryManga : MangaImpl() {
|
||||||
|
|
||||||
var unread: Int = 0
|
var unreadCount: Int = 0
|
||||||
|
var readCount: Int = 0
|
||||||
|
|
||||||
|
val totalChapters
|
||||||
|
get() = readCount + unreadCount
|
||||||
|
|
||||||
|
val hasStarted
|
||||||
|
get() = readCount > 0
|
||||||
|
|
||||||
var category: Int = 0
|
var category: Int = 0
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,6 @@ fun Manga.toMangaInfo(): MangaInfo {
|
|||||||
genres = this.getGenres() ?: emptyList(),
|
genres = this.getGenres() ?: emptyList(),
|
||||||
key = this.url,
|
key = this.url,
|
||||||
status = this.status,
|
status = this.status,
|
||||||
title = this.title
|
title = this.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface CategoryQueries : DbProvider {
|
|||||||
Query.builder()
|
Query.builder()
|
||||||
.table(CategoryTable.TABLE)
|
.table(CategoryTable.TABLE)
|
||||||
.orderBy(CategoryTable.COL_ORDER)
|
.orderBy(CategoryTable.COL_ORDER)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ interface CategoryQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getCategoriesForMangaQuery())
|
.query(getCategoriesForMangaQuery())
|
||||||
.args(manga.id)
|
.args(manga.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(mangaId)
|
.whereArgs(mangaId)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -37,7 +37,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.query(getRecentsQuery())
|
.query(getRecentsQuery())
|
||||||
.args(date.time)
|
.args(date.time)
|
||||||
.observesTables(ChapterTable.TABLE)
|
.observesTables(ChapterTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
@@ -49,7 +49,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_ID} = ?")
|
.where("${ChapterTable.COL_ID} = ?")
|
||||||
.whereArgs(id)
|
.whereArgs(id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
.whereArgs(url)
|
.whereArgs(url)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(url, mangaId)
|
.whereArgs(url, mangaId)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
.whereArgs(url)
|
.whereArgs(url)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ interface ChapterQueries : DbProvider {
|
|||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
|
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
|
||||||
.whereArgs(urls.joinToString { "\"$it\"" })
|
.whereArgs(urls.joinToString { "\"$it\"" })
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.HistoryChapterIdPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
@@ -32,7 +33,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.query(getRecentMangasQuery(search))
|
.query(getRecentMangasQuery(search))
|
||||||
.args(date.time, limit, offset)
|
.args(date.time, limit, offset)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
@@ -44,7 +45,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.query(getHistoryByMangaId())
|
.query(getHistoryByMangaId())
|
||||||
.args(mangaId)
|
.args(mangaId)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.query(getHistoryByChapterUrl())
|
.query(getHistoryByChapterUrl())
|
||||||
.args(chapterUrl)
|
.args(chapterUrl)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ interface HistoryQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -93,7 +94,24 @@ interface HistoryQueries : DbProvider {
|
|||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||||
.whereArgs(0)
|
.whereArgs(0)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun updateHistoryChapterIds(history: List<History>) = db.put()
|
||||||
|
.objects(history)
|
||||||
|
.withPutResolver(HistoryChapterIdPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun deleteHistoryIds(ids: List<Long>) = db.delete()
|
||||||
|
.byQuery(
|
||||||
|
DeleteQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.where("${HistoryTable.COL_ID} IN (?)")
|
||||||
|
.whereArgs(ids.joinToString())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface MangaCategoryQueries : DbProvider {
|
|||||||
.table(MangaCategoryTable.TABLE)
|
.table(MangaCategoryTable.TABLE)
|
||||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,26 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(libraryQuery)
|
.query(libraryQuery)
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getDuplicateLibraryManga(manga: Manga) = db.get()
|
||||||
|
.`object`(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
|
||||||
|
.whereArgs(
|
||||||
|
manga.title.lowercase(),
|
||||||
|
manga.source,
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||||
var queryBuilder = Query.builder()
|
var queryBuilder = Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
@@ -63,7 +78,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||||
.whereArgs(url, sourceId)
|
.whereArgs(url, sourceId)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -74,7 +89,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(id)
|
.whereArgs(id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -84,7 +99,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getSourceIdsWithNonLibraryMangaQuery())
|
.query(getSourceIdsWithNonLibraryMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
@@ -95,7 +110,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -104,7 +119,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.withQuery(
|
.withQuery(
|
||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getReadMangaNotInLibraryQuery())
|
.query(getReadMangaNotInLibraryQuery())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -194,11 +209,11 @@ interface MangaQueries : DbProvider {
|
|||||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
|
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
|
||||||
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
.whereArgs(0, *sourceIds.toTypedArray())
|
.whereArgs(0, *sourceIds.toTypedArray())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -214,10 +229,10 @@ interface MangaQueries : DbProvider {
|
|||||||
) AND ${MangaTable.COL_ID} NOT IN (
|
) AND ${MangaTable.COL_ID} NOT IN (
|
||||||
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
.whereArgs(0)
|
.whereArgs(0, *sourceIds.toTypedArray())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -226,7 +241,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -236,7 +251,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getLastReadMangaQuery())
|
.query(getLastReadMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -246,7 +261,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getTotalChapterMangaQuery())
|
.query(getTotalChapterMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -256,7 +271,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getLatestChapterMangaQuery())
|
.query(getLatestChapterMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -266,7 +281,7 @@ interface MangaQueries : DbProvider {
|
|||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getChapterFetchDateMangaQuery())
|
.query(getChapterFetchDateMangaQuery())
|
||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -281,9 +296,9 @@ interface MangaQueries : DbProvider {
|
|||||||
INNER JOIN ${SearchMetadataTable.TABLE}
|
INNER JOIN ${SearchMetadataTable.TABLE}
|
||||||
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
|
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
|
||||||
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
|
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -298,9 +313,9 @@ interface MangaQueries : DbProvider {
|
|||||||
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
|
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
|
||||||
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
|
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
|
||||||
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
|
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -315,9 +330,9 @@ interface MangaQueries : DbProvider {
|
|||||||
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
|
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
|
||||||
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
|
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
|
||||||
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
|
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||||
|
import exh.savedsearches.tables.FeedSavedSearchTable
|
||||||
|
import exh.savedsearches.tables.SavedSearchTable
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
@@ -75,23 +77,49 @@ fun getReadMangaNotInLibraryQuery() =
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the manga from the library, with their categories and unread count.
|
* Query to get the global feed saved searches
|
||||||
|
*/
|
||||||
|
fun getGlobalFeedSavedSearchQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${SavedSearchTable.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 1
|
||||||
|
) AS M
|
||||||
|
JOIN ${SavedSearchTable.TABLE}
|
||||||
|
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the source feed saved searches
|
||||||
|
*/
|
||||||
|
fun getSourceFeedSavedSearchQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${SavedSearchTable.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 0 AND ${FeedSavedSearchTable.COL_SOURCE} = ?
|
||||||
|
) AS M
|
||||||
|
JOIN ${SavedSearchTable.TABLE}
|
||||||
|
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the manga from the library, with their categories, read and unread count.
|
||||||
*/
|
*/
|
||||||
val libraryQuery =
|
val libraryQuery =
|
||||||
"""
|
"""
|
||||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT}
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unreadCount
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 0
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
) AS C
|
) AS C
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS readCount
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 1
|
WHERE ${Chapter.COL_READ} = 1
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
@@ -100,10 +128,10 @@ val libraryQuery =
|
|||||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
UNION
|
UNION
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT}
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
|
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unreadCount
|
||||||
FROM ${Merged.TABLE}
|
FROM ${Merged.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||||
@@ -112,7 +140,7 @@ val libraryQuery =
|
|||||||
) AS C
|
) AS C
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
|
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as readCount
|
||||||
FROM ${Merged.TABLE}
|
FROM ${Merged.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface TrackQueries : DbProvider {
|
|||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ interface TrackQueries : DbProvider {
|
|||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ interface TrackQueries : DbProvider {
|
|||||||
.table(TrackTable.TABLE)
|
.table(TrackTable.TABLE)
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||||
.whereArgs(manga.id, sync.id)
|
.whereArgs(manga.id, sync.id)
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -29,6 +29,6 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_READ to chapter.read,
|
ChapterTable.COL_READ to chapter.read,
|
||||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -29,6 +29,6 @@ class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_READ to chapter.read,
|
ChapterTable.COL_READ to chapter.read,
|
||||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -29,6 +29,6 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_READ to chapter.read,
|
ChapterTable.COL_READ to chapter.read,
|
||||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) =
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
|
ChapterTable.COL_SOURCE_ORDER to chapter.source_order,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
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.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
|
||||||
|
class HistoryChapterIdPutResolver : PutResolver<History>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, history: History) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(history)
|
||||||
|
val contentValues = mapToContentValues(history)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(history: History) = UpdateQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.where("${HistoryTable.COL_ID} = ?")
|
||||||
|
.whereArgs(history.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(history: History) =
|
||||||
|
contentValuesOf(
|
||||||
|
HistoryTable.COL_CHAPTER_ID to history.chapter_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -24,7 +24,7 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
.table(updateQuery.table())
|
.table(updateQuery.table())
|
||||||
.where(updateQuery.where())
|
.where(updateQuery.where())
|
||||||
.whereArgs(updateQuery.whereArgs())
|
.whereArgs(updateQuery.whereArgs())
|
||||||
.build()
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
||||||
cursor.use { putCursor ->
|
cursor.use { putCursor ->
|
||||||
@@ -47,6 +47,6 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
|
|
||||||
private fun mapToUpdateContentValues(history: History) =
|
private fun mapToUpdateContentValues(history: History) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
HistoryTable.COL_LAST_READ to history.last_read
|
HistoryTable.COL_LAST_READ to history.last_read,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -16,11 +16,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
val manga = LibraryManga()
|
val manga = LibraryManga()
|
||||||
|
|
||||||
mapBaseFromCursor(manga, cursor)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
|
manga.unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_UNREAD_COUNT))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
||||||
// SY -->
|
manga.readCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_READ_COUNT))
|
||||||
manga.read = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_READ))
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
|
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -28,6 +28,6 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_FAVORITE to manga.favorite,
|
MangaTable.COL_FAVORITE to manga.favorite,
|
||||||
MangaTable.COL_DATE_ADDED to manga.date_added
|
MangaTable.COL_DATE_ADDED to manga.date_added,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators
|
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -28,6 +28,6 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
colName to fieldGetter.get(manga)
|
colName to fieldGetter.get(manga),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
||||||
MangaTable.COL_ARTIST to manga.originalArtist,
|
MangaTable.COL_ARTIST to manga.originalArtist,
|
||||||
MangaTable.COL_DESCRIPTION to manga.originalDescription,
|
MangaTable.COL_DESCRIPTION to manga.originalDescription,
|
||||||
MangaTable.COL_STATUS to manga.originalStatus
|
MangaTable.COL_STATUS to manga.originalStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun resetToContentValues(manga: Manga) = contentValuesOf(
|
private fun resetToContentValues(manga: Manga) = contentValuesOf(
|
||||||
@@ -41,7 +41,7 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
|
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
|
||||||
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
|
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
|
||||||
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
|
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
|
||||||
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull()
|
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull(),
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_LAST_UPDATE to manga.last_update
|
MangaTable.COL_LAST_UPDATE to manga.last_update,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -30,6 +30,6 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_DATE_ADDED to manga.date_added,
|
MangaTable.COL_DATE_ADDED to manga.date_added,
|
||||||
MangaTable.COL_TITLE to manga.title,
|
MangaTable.COL_TITLE to manga.title,
|
||||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
||||||
MangaTable.COL_VIEWER to manga.viewer_flags
|
MangaTable.COL_VIEWER to manga.viewer_flags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class MangaThumbnailPutResolver : PutResolver<Manga>() {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url
|
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_TITLE to manga.title
|
MangaTable.COL_TITLE to manga.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,6 +27,6 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
MangaTable.COL_URL to manga.url
|
MangaTable.COL_URL to manga.url,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,7 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
||||||
|
|
||||||
const val COL_UNREAD = "unread"
|
// SY -->
|
||||||
|
|
||||||
// SY ->>
|
|
||||||
const val COL_READ = "read"
|
|
||||||
|
|
||||||
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
|
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -51,6 +47,11 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||||
|
|
||||||
|
// Not an actual value but computed when created
|
||||||
|
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
|
||||||
|
|
||||||
|
const val COMPUTED_COL_READ_COUNT = "read_count"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() =
|
get() =
|
||||||
"""CREATE TABLE $TABLE(
|
"""CREATE TABLE $TABLE(
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ object TrackTable {
|
|||||||
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
||||||
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||||
|FROM ${TABLE}_tmp
|
|FROM ${TABLE}_tmp
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
|
|
||||||
val dropTempTable: String
|
val dropTempTable: String
|
||||||
get() = "DROP TABLE ${TABLE}_tmp"
|
get() = "DROP TABLE ${TABLE}_tmp"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class DownloadCache(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,7 +251,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private class RootDirectory(
|
private class RootDirectory(
|
||||||
val dir: UniFile,
|
val dir: UniFile,
|
||||||
var files: Map<Long, SourceDirectory> = hashMapOf()
|
var files: Map<Long, SourceDirectory> = hashMapOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,7 +259,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private class SourceDirectory(
|
private class SourceDirectory(
|
||||||
val dir: UniFile,
|
val dir: UniFile,
|
||||||
var files: Map<String, MangaDirectory> = hashMapOf()
|
var files: Map<String, MangaDirectory> = hashMapOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,7 +267,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private class MangaDirectory(
|
private class MangaDirectory(
|
||||||
val dir: UniFile,
|
val dir: UniFile,
|
||||||
var files: Set<String> = hashSetOf()
|
var files: Set<String> = hashSetOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class DownloadManager(
|
class DownloadManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val db: DatabaseHelper = Injekt.get()
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|||||||
@@ -93,14 +93,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_pause_24dp,
|
R.drawable.ic_pause_24dp,
|
||||||
context.getString(R.string.action_pause),
|
context.getString(R.string.action_pause),
|
||||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
|
NotificationReceiver.pauseDownloadsPendingBroadcast(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadingProgressText = context.getString(
|
val downloadingProgressText = context.getString(
|
||||||
R.string.chapter_downloading_progress,
|
R.string.chapter_downloading_progress,
|
||||||
download.downloadedImages,
|
download.downloadedImages,
|
||||||
download.pages!!.size
|
download.pages!!.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
@@ -138,13 +138,13 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_play_arrow_24dp,
|
R.drawable.ic_play_arrow_24dp,
|
||||||
context.getString(R.string.action_resume),
|
context.getString(R.string.action_resume),
|
||||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
NotificationReceiver.resumeDownloadsPendingBroadcast(context),
|
||||||
)
|
)
|
||||||
// Clear action
|
// Clear action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
context.getString(R.string.action_cancel_all),
|
context.getString(R.string.action_cancel_all),
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
@@ -184,16 +184,19 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* Called when the downloader receives a warning.
|
* Called when the downloader receives a warning.
|
||||||
*
|
*
|
||||||
* @param reason the text to show.
|
* @param reason the text to show.
|
||||||
|
* @param timeout duration after which to automatically dismiss the notification.
|
||||||
|
* Only works on Android 8+.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String, timeout: Long? = null) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
setContentText(reason)
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
timeout?.let { setTimeoutAfter(it) }
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
@@ -213,7 +216,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
// Create notification
|
// Create notification
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
|
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title),
|
||||||
)
|
)
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
@Serializable
|
@Serializable
|
||||||
private data class Entry(
|
private data class Entry(
|
||||||
val chapters: List<ChapterEntry>,
|
val chapters: List<ChapterEntry>,
|
||||||
val manga: MangaEntry
|
val manga: MangaEntry,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val scanlator: String? = null
|
val scanlator: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,14 +148,14 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val source: Long
|
val source: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a manga entry from a manga model.
|
* Returns a manga entry from a manga model.
|
||||||
*/
|
*/
|
||||||
private fun Manga.toEntry(): MangaEntry {
|
private fun Manga.toEntry(): MangaEntry {
|
||||||
return MangaEntry(id!!, url, title, source)
|
return MangaEntry(id!!, url, originalTitle, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findUnmatchedChapterDirs(
|
fun findUnmatchedChapterDirs(
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source
|
source: Source,
|
||||||
): List<UniFile> {
|
): List<UniFile> {
|
||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return mangaDir.listFiles().orEmpty().asList().filter {
|
return mangaDir.listFiles().orEmpty().asList().filter {
|
||||||
@@ -164,7 +164,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
when {
|
when {
|
||||||
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
||||||
else -> chapter.name
|
else -> chapter.name
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
"$chapterName.cbz",
|
"$chapterName.cbz",
|
||||||
|
|
||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,9 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun listenDownloaderState() {
|
private fun listenDownloaderState() {
|
||||||
subscriptions += downloadManager.runningRelay
|
subscriptions += downloadManager.runningRelay
|
||||||
.doOnError { /* Swallow wakelock error */ }
|
.doOnError {
|
||||||
|
/* Swallow wakelock error */
|
||||||
|
}
|
||||||
.subscribe { running ->
|
.subscribe { running ->
|
||||||
if (running) {
|
if (running) {
|
||||||
wakeLock.acquireIfNeeded()
|
wakeLock.acquireIfNeeded()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class DownloadStore(
|
class DownloadStore(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
@@ -59,7 +60,7 @@ class Downloader(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val cache: DownloadCache,
|
private val cache: DownloadCache,
|
||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val chapterCache: ChapterCache by injectLazy()
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
@@ -210,7 +211,7 @@ class Downloader(
|
|||||||
downloadChapter(download).subscribeOn(Schedulers.io())
|
downloadChapter(download).subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
5
|
5,
|
||||||
)
|
)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
@@ -222,7 +223,7 @@ class Downloader(
|
|||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
logcat(LogPriority.ERROR, error)
|
logcat(LogPriority.ERROR, error)
|
||||||
notifier.onError(error.message)
|
notifier.onError(error.message)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,15 +274,21 @@ class Downloader(
|
|||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
|
val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count()
|
||||||
val maxDownloadsFromSource = queue
|
val maxDownloadsFromSource = queue
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.filterKeys { it !is UnmeteredSource }
|
.filterKeys { it !is UnmeteredSource }
|
||||||
.maxOf { it.value.size }
|
.maxOf { it.value.size }
|
||||||
// TODO: re-enable warning
|
if (
|
||||||
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
||||||
// withUIContext {
|
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||||
// context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)
|
) {
|
||||||
// }
|
withUIContext {
|
||||||
|
notifier.onWarning(
|
||||||
|
context.getString(R.string.download_queue_size_warning),
|
||||||
|
WARNING_NOTIF_TIMEOUT_MS,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DownloadService.start(context)
|
DownloadService.start(context)
|
||||||
}
|
}
|
||||||
@@ -482,7 +489,7 @@ class Downloader(
|
|||||||
download: Download,
|
download: Download,
|
||||||
mangaDir: UniFile,
|
mangaDir: UniFile,
|
||||||
tmpDir: UniFile,
|
tmpDir: UniFile,
|
||||||
dirname: String
|
dirname: String,
|
||||||
) {
|
) {
|
||||||
// Ensure that the chapter folder has all the images.
|
// Ensure that the chapter folder has all the images.
|
||||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||||
@@ -563,9 +570,11 @@ class Downloader(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TMP_DIR_SUFFIX = "_tmp"
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
|
const val WARNING_NOTIF_TIMEOUT_MS = 30_000L
|
||||||
const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15
|
const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15
|
||||||
|
private const val DOWNLOADS_QUEUED_WARNING_THRESHOLD = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arbitrary minimum required space to start a download: 50 MB
|
// Arbitrary minimum required space to start a download: 200 MB
|
||||||
private const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
private const val MIN_DISK_SPACE = 200L * 1024 * 1024
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import java.util.concurrent.CopyOnWriteArrayList
|
|||||||
|
|
||||||
class DownloadQueue(
|
class DownloadQueue(
|
||||||
private val store: DownloadStore,
|
private val store: DownloadStore,
|
||||||
private val queue: MutableList<Download> = CopyOnWriteArrayList()
|
private val queue: MutableList<Download> = CopyOnWriteArrayList(),
|
||||||
) : List<Download> by queue {
|
) : List<Download> by queue {
|
||||||
|
|
||||||
private val statusSubject = PublishSubject.create<Download>()
|
private val statusSubject = PublishSubject.create<Download>()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
val json = try {
|
val json = try {
|
||||||
Json.decodeFromString<MangaList>(
|
Json.decodeFromString<MangaList>(
|
||||||
editJson.bufferedReader().use { it.readText() }
|
editJson.bufferedReader().use { it.readText() },
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
@@ -67,13 +67,13 @@ class CustomMangaManager(val context: Context) {
|
|||||||
artist,
|
artist,
|
||||||
description,
|
description,
|
||||||
genre?.split(", "),
|
genre?.split(", "),
|
||||||
status
|
status,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MangaList(
|
data class MangaList(
|
||||||
val mangas: List<MangaJson>? = null
|
val mangas: List<MangaJson>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -84,7 +84,7 @@ class CustomMangaManager(val context: Context) {
|
|||||||
val artist: String? = null,
|
val artist: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val genre: List<String>? = null,
|
val genre: List<String>? = null,
|
||||||
val status: Int? = null
|
val status: Int? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toManga() = MangaImpl().apply {
|
fun toManga() = MangaImpl().apply {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
interval.toLong(),
|
interval.toLong(),
|
||||||
TimeUnit.HOURS,
|
TimeUnit.HOURS,
|
||||||
10,
|
10,
|
||||||
TimeUnit.MINUTES
|
TimeUnit.MINUTES,
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.Downloader
|
import eu.kanade.tachiyomi.data.download.Downloader
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@@ -86,31 +87,67 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
Notifications.ID_LIBRARY_PROGRESS,
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build()
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showQueueSizeWarningNotification() {
|
||||||
|
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
|
setContentTitle(context.getString(R.string.label_warning))
|
||||||
|
setContentText(context.getString(R.string.notification_size_warning))
|
||||||
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
|
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_SIZE_WARNING,
|
||||||
|
notificationBuilder.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows notification containing update entries that failed with action to open full log.
|
* Shows notification containing update entries that failed with action to open full log.
|
||||||
*
|
*
|
||||||
* @param errors List of entry titles that failed to update.
|
* @param failed Number of entries that failed to update.
|
||||||
* @param uri Uri for error log file containing all titles that failed.
|
* @param uri Uri for error log file containing all titles that failed.
|
||||||
*/
|
*/
|
||||||
fun showUpdateErrorNotification(errors: List<String>, uri: Uri) {
|
fun showUpdateErrorNotification(failed: Int, uri: Uri) {
|
||||||
if (errors.isEmpty()) {
|
if (failed == 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_ERROR,
|
Notifications.ID_LIBRARY_ERROR,
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
setContentTitle(context.resources.getString(R.string.notification_update_error, failed))
|
||||||
setContentText(context.getString(R.string.action_show_errors))
|
setContentText(context.getString(R.string.action_show_errors))
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
||||||
}
|
}
|
||||||
.build()
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows notification containing update entries that were skipped.
|
||||||
|
*
|
||||||
|
* @param skipped Number of entries that were skipped during the update.
|
||||||
|
*/
|
||||||
|
fun showUpdateSkippedNotification(skipped: Int) {
|
||||||
|
if (skipped == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
context.notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_SKIPPED,
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_SKIPPED) {
|
||||||
|
setContentTitle(context.resources.getString(R.string.notification_update_skipped, skipped))
|
||||||
|
setContentText(context.getString(R.string.learn_more))
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +177,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
NotificationCompat.BigTextStyle().bigText(
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
updates.joinToString("\n") {
|
updates.joinToString("\n") {
|
||||||
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
|
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +193,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
setContentIntent(getNotificationIntent())
|
setContentIntent(getNotificationIntent())
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Per-manga notification
|
// Per-manga notification
|
||||||
@@ -201,8 +238,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
context,
|
context,
|
||||||
manga,
|
manga,
|
||||||
chapters,
|
chapters,
|
||||||
Notifications.ID_NEW_CHAPTERS
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
// View chapters action
|
// View chapters action
|
||||||
addAction(
|
addAction(
|
||||||
@@ -211,8 +248,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
NotificationReceiver.openChapterPendingActivity(
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
context,
|
context,
|
||||||
manga,
|
manga,
|
||||||
Notifications.ID_NEW_CHAPTERS
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
// Download chapters action
|
// Download chapters action
|
||||||
// Only add the action when chapters is within threshold
|
// Only add the action when chapters is within threshold
|
||||||
@@ -224,8 +261,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
context,
|
context,
|
||||||
manga,
|
manga,
|
||||||
chapters,
|
chapters,
|
||||||
Notifications.ID_NEW_CHAPTERS
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +289,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
val formatter = DecimalFormat(
|
val formatter = DecimalFormat(
|
||||||
"#.###",
|
"#.###",
|
||||||
DecimalFormatSymbols()
|
DecimalFormatSymbols()
|
||||||
.apply { decimalSeparator = '.' }
|
.apply { decimalSeparator = '.' },
|
||||||
)
|
)
|
||||||
|
|
||||||
val displayableChapterNumbers = chapters
|
val displayableChapterNumbers = chapters
|
||||||
@@ -304,10 +341,9 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
|
||||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
|
||||||
private const val NOTIF_ICON_SIZE = 192
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
|
private const val NOTIF_ICON_SIZE = 192
|
||||||
|
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/help/faq/#why-does-global-update-skip-some-entries"
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ 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.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
|
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.GroupLibraryMode
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.GroupLibraryMode
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
@@ -89,7 +90,7 @@ class LibraryUpdateService(
|
|||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get(),
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val trackManager: TrackManager = Injekt.get(),
|
val trackManager: TrackManager = Injekt.get(),
|
||||||
val coverCache: CoverCache = Injekt.get()
|
val coverCache: CoverCache = Injekt.get(),
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
@@ -171,7 +172,7 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra, target)
|
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +246,7 @@ class LibraryUpdateService(
|
|||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||||
val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA)
|
val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA)
|
||||||
addMangaToQueue(categoryId, group, groupExtra, target)
|
addMangaToQueue(categoryId, group, groupExtra)
|
||||||
|
|
||||||
// Destroy service when completed or in case of an error.
|
// Destroy service when completed or in case of an error.
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
@@ -274,13 +275,13 @@ class LibraryUpdateService(
|
|||||||
* @param category the ID of the category to update, or -1 if no category specified.
|
* @param category the ID of the category to update, or -1 if no category specified.
|
||||||
* @param target the target to update.
|
* @param target the target to update.
|
||||||
*/
|
*/
|
||||||
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?, target: Target) {
|
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
|
||||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
// SY -->
|
// SY -->
|
||||||
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1) {
|
val listToUpdate = if (categoryId != -1) {
|
||||||
libraryManga.filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
// SY -->
|
// SY -->
|
||||||
} else if (
|
} else if (
|
||||||
@@ -307,20 +308,16 @@ class LibraryUpdateService(
|
|||||||
when (group) {
|
when (group) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||||
libraryManga.filter {
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||||
val status: String = run {
|
val statuses = loggedServices.associate {
|
||||||
val tracks = db.getTracks(it).executeAsBlocking()
|
it.id to it.getStatusList().associateWith(it::getStatus)
|
||||||
val track = tracks.find { track ->
|
}
|
||||||
loggedServices.any { it.id == track?.sync_id }
|
|
||||||
}
|
libraryManga.filter { manga ->
|
||||||
val service = loggedServices.find { it.id == track?.sync_id }
|
val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
|
||||||
if (track != null && service != null) {
|
statuses[track.sync_id]?.get(track.status)
|
||||||
service.getStatus(track.status)
|
} ?: "not tracked"
|
||||||
} else {
|
|
||||||
"not tracked"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
|
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,16 +342,6 @@ class LibraryUpdateService(
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target == Target.CHAPTERS) {
|
|
||||||
val restrictions = preferences.libraryUpdateMangaRestriction().get()
|
|
||||||
if (MANGA_ONGOING in restrictions) {
|
|
||||||
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
|
||||||
}
|
|
||||||
if (MANGA_FULLY_READ in restrictions) {
|
|
||||||
listToUpdate = listToUpdate.filter { it.unread == 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mangaToUpdate = listToUpdate
|
mangaToUpdate = listToUpdate
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
.sortedBy { it.title }
|
.sortedBy { it.title }
|
||||||
@@ -364,9 +351,8 @@ class LibraryUpdateService(
|
|||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
|
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
|
||||||
.maxOfOrNull { it.value.size } ?: 0
|
.maxOfOrNull { it.value.size } ?: 0
|
||||||
// TODO: re-enable warning
|
|
||||||
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||||
// toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
|
notifier.showQueueSizeWarningNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,10 +370,12 @@ class LibraryUpdateService(
|
|||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
|
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
|
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
|
||||||
|
val restrictions = preferences.libraryUpdateMangaRestriction().get()
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
mangaToUpdate.groupBy { it.source }
|
mangaToUpdate.groupBy { it.source }
|
||||||
@@ -407,19 +395,33 @@ class LibraryUpdateService(
|
|||||||
manga,
|
manga,
|
||||||
) { manga ->
|
) { manga ->
|
||||||
try {
|
try {
|
||||||
val (newChapters, _) = updateManga(manga, loggedServices)
|
when {
|
||||||
|
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
|
||||||
if (newChapters.isNotEmpty()) {
|
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
|
||||||
downloadChapters(manga, newChapters)
|
|
||||||
hasDownloads.set(true)
|
|
||||||
}
|
}
|
||||||
|
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
|
||||||
|
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
|
||||||
|
}
|
||||||
|
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
|
||||||
|
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Convert to the manga that contains new chapters
|
||||||
|
val (newChapters, _) = updateManga(manga, loggedServices)
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
if (newChapters.isNotEmpty()) {
|
||||||
newUpdates.add(
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
downloadChapters(manga, newChapters)
|
||||||
.toTypedArray()
|
hasDownloads.set(true)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// Convert to the manga that contains new chapters
|
||||||
|
newUpdates.add(
|
||||||
|
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||||
|
.toTypedArray(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
val errorMessage = when (e) {
|
val errorMessage = when (e) {
|
||||||
@@ -462,10 +464,13 @@ class LibraryUpdateService(
|
|||||||
if (failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
val errorFile = writeErrorFile(failedUpdates)
|
val errorFile = writeErrorFile(failedUpdates)
|
||||||
notifier.showUpdateErrorNotification(
|
notifier.showUpdateErrorNotification(
|
||||||
failedUpdates.map { it.first.title },
|
failedUpdates.size,
|
||||||
errorFile.getUriCompat(this)
|
errorFile.getUriCompat(this),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (skippedUpdates.isNotEmpty()) {
|
||||||
|
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
@@ -639,7 +644,7 @@ class LibraryUpdateService(
|
|||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.get(),
|
||||||
mangaToUpdate.size
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
block(manga)
|
block(manga)
|
||||||
@@ -653,7 +658,7 @@ class LibraryUpdateService(
|
|||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.get(),
|
||||||
mangaToUpdate.size
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +691,7 @@ class LibraryUpdateService(
|
|||||||
dbManga = Manga.create(
|
dbManga = Manga.create(
|
||||||
networkManga.url,
|
networkManga.url,
|
||||||
networkManga.title,
|
networkManga.title,
|
||||||
mangaDex.id
|
mangaDex.id,
|
||||||
)
|
)
|
||||||
dbManga.date_added = System.currentTimeMillis()
|
dbManga.date_added = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that manages [PendingIntent] of activity's
|
* Class that manages [PendingIntent] of activity's
|
||||||
@@ -23,7 +21,7 @@ object NotificationHandler {
|
|||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
action = MainActivity.SHORTCUT_DOWNLOADS
|
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,13 +30,12 @@ object NotificationHandler {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param file file containing image
|
* @param file file containing image
|
||||||
*/
|
*/
|
||||||
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
val uri = file.getUriCompat(context)
|
|
||||||
setDataAndType(uri, "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
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +49,11 @@ object NotificationHandler {
|
|||||||
setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||||
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
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openUrl(context: Context, url: String): PendingIntent {
|
||||||
|
val notificationIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
return PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user