mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 11:07:51 +02:00
Compare commits
165 Commits
Author | SHA1 | Date | |
---|---|---|---|
0f3f1e9226 | |||
79ab492a5b | |||
62db4bb09d | |||
7be2cbb75b | |||
5b1fe3460f | |||
31997fe50a | |||
5e5ceef122 | |||
40edbac7f0 | |||
5bb1f72c28 | |||
8622e6492c | |||
1feac9c559 | |||
fce81dd6d9 | |||
aa50554f06 | |||
034506f56b | |||
2d8858edb4 | |||
b2601ad696 | |||
8099f561c5 | |||
8a014ddb0c | |||
3d9383ce67 | |||
9de07c11a6 | |||
9f744bc445 | |||
aed6e12119 | |||
c57d0046bc | |||
07b9fc9b31 | |||
2c6bcb85a0 | |||
fefa519486 | |||
11a232a2df | |||
8dcd919ff0 | |||
d9c27e7109 | |||
8af8c57bb4 | |||
a1a4916abf | |||
9be8f675ac | |||
a271c3726e | |||
8c18a14dfd | |||
9a801cfdfb | |||
4af13e3536 | |||
e76e903060 | |||
3d89a317c1 | |||
d8251224cb | |||
acd927a937 | |||
a498f940c6 | |||
948cb31d1a | |||
179cb8eb50 | |||
47f865aa72 | |||
b47face2f8 | |||
69869115f6 | |||
0fb9ca3e8b | |||
eaf9c9b2d8 | |||
70d9b0c390 | |||
e57a999c9c | |||
3b49289cfb | |||
176e984b56 | |||
b5a700276a | |||
3c186a3c8d | |||
a462ce3626 | |||
065cf42aea | |||
986b709f2c | |||
fed6f44995 | |||
1b52acdad7 | |||
10a638c6b8 | |||
7875f363a8 | |||
685736b9ec | |||
aefd2bf6f8 | |||
ce9fb2f1fe | |||
974275a429 | |||
98461f9bca | |||
094f78fb41 | |||
33dcdc1599 | |||
8870ccb18c | |||
2a7ed1375a | |||
107727eea9 | |||
54b50cca71 | |||
1c10ba7925 | |||
2b8df691ff | |||
15da856303 | |||
cef5343a24 | |||
f96b85fcb2 | |||
a62628423f | |||
ef8a87a30f | |||
89fb943733 | |||
147978b932 | |||
c741920ec0 | |||
bbbcb18b91 | |||
d6b3b0baf7 | |||
dbe8931cf0 | |||
d2eb5d7f45 | |||
562dce60ee | |||
569df39fb8 | |||
2f7f00c7a2 | |||
afd59eabbb | |||
cf99446a12 | |||
68286b2acc | |||
a410184e0a | |||
d3ceecf620 | |||
940c5b3838 | |||
17c321286d | |||
0dbb79359b | |||
19f39fcdb0 | |||
ab021c1302 | |||
3b11ad8de8 | |||
cf4b870846 | |||
5e37f72d74 | |||
6843dbf7e1 | |||
09c07faafd | |||
8e7c235ff0 | |||
7fb4cbb8a0 | |||
fa872f6cf7 | |||
ef53d4ec07 | |||
c68e7c8da7 | |||
de35a4c62a | |||
fcde6c2b84 | |||
9cbe053e79 | |||
818468c58f | |||
7ba43ae5c2 | |||
5700c7a0c7 | |||
4bfd395d9f | |||
5069d8dee6 | |||
47c120e58c | |||
8d7ab13f5c | |||
122cdae5bc | |||
157d8db68c | |||
998da965cd | |||
8d58a8d548 | |||
b453be081e | |||
3c947f323f | |||
cb203ef02c | |||
908c9bc624 | |||
fe373a95a2 | |||
60f18f3b5a | |||
284c019b32 | |||
32434471e5 | |||
6a4c280235 | |||
f0eacf4218 | |||
0afe3011bc | |||
0fef546a0d | |||
93e6136795 | |||
7d23fd8ef5 | |||
71c9df5279 | |||
224fcada17 | |||
9278407b85 | |||
dad3292bdd | |||
cfdf319972 | |||
89619b7836 | |||
6aff438a16 | |||
13324dd1a1 | |||
ae9bf06b46 | |||
5236834911 | |||
bf80dd622c | |||
662b71436e | |||
f608cb55eb | |||
6ba82da029 | |||
f407e30b6e | |||
4e7b8c98f9 | |||
5f9574541f | |||
08a6db7d6e | |||
b485e1d657 | |||
e8d8621f06 | |||
4cefbce7c3 | |||
fa31369f99 | |||
d0bf93ebb7 | |||
41a747c7e7 | |||
8882cd4787 | |||
6676490e09 | |||
68bea8a196 | |||
25995c09a0 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.10.10)
|
||||
- To the latest version of the app (stable is v0.11.1)
|
||||
- All extensions
|
||||
- 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
|
||||
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -10,7 +10,7 @@ labels: "bug"
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.10.10)
|
||||
- To the latest version of the app (stable is v0.11.1)
|
||||
- All extensions
|
||||
- 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
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -10,7 +10,7 @@ labels: "feature"
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.10.10)
|
||||
- To the latest version of the app (stable is v0.11.1)
|
||||
- All extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
|
2
.github/workflows/issue_closer.yml
vendored
2
.github/workflows/issue_closer.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.0
|
||||
uses: arkon/issue-closer-action@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
rules: |
|
||||
|
14
.github/workflows/issue_moderator.yml
vendored
Normal file
14
.github/workflows/issue_moderator.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
@ -1,31 +0,0 @@
|
||||
### r2903
|
||||
- The MyAnimeList tracker was rewritten. You will need to log out and log in again.
|
||||
|
||||
### r1810
|
||||
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
|
||||
run properly. This includes app updates, library updates, and automatic backups.
|
||||
|
||||
### r1340
|
||||
- A new screen for managing extensions was added. If you previously installed extensions from FDroid,
|
||||
you will have to uninstall all of them first (tap on the extension then uninstall), otherwise you won't be able
|
||||
to update them due to signature mismatch. You won't lose anything in this process as the extensions themselves
|
||||
don't store anything.
|
||||
|
||||
### r959
|
||||
- The download manager has been rewritten and it's possible some of your downloads
|
||||
aren't recognized anymore. You may have to check your downloads folder and manually delete those.
|
||||
- You can now download to any folder in your SD card.
|
||||
- The download directory setting has been reset.
|
||||
|
||||
### r857
|
||||
- **Important!** Delete after read has been updated.
|
||||
This means the value has been reset set to disabled.
|
||||
This can be changed in Settings > Downloads
|
||||
|
||||
### r736
|
||||
- **Important!** Now chapters follow the order of the sources. **It's required that you update your entire library
|
||||
before reading in order for them to be synced.** Old behavior can be restored for a manga in the overflow menu of the chapters tab.
|
||||
|
||||
### r724
|
||||
- Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
|
||||
from the info tab, or clearing the database (the latter won't fix covers from library manga).
|
@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
# Tachiyomi
|
||||
Tachiyomi is a free and open source manga reader for Android 5.0 and above.
|
||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||
|
||||

|
||||
|
||||
|
@ -8,7 +8,6 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
}
|
||||
@ -29,8 +28,8 @@ android {
|
||||
minSdkVersion(AndroidConfig.minSdk)
|
||||
targetSdkVersion(AndroidConfig.targetSdk)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 58
|
||||
versionName = "0.10.11"
|
||||
versionCode = 63
|
||||
versionName = "0.11.1"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -55,20 +54,27 @@ android {
|
||||
named("debug") {
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
}
|
||||
create("debugFull") { // Debug without R8
|
||||
initWith(getByName("debug"))
|
||||
isShrinkResources = false
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
named("release") {
|
||||
/*named("postprocessing") {
|
||||
postprocessing {
|
||||
isObfuscate = false
|
||||
isOptimizeCode = true
|
||||
isRemoveUnusedCode = false
|
||||
isRemoveUnusedResources = true
|
||||
}
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||
}*/
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("debugFull").res.srcDirs("src/debug/res")
|
||||
}
|
||||
|
||||
flavorDimensions("default")
|
||||
|
||||
productFlavors {
|
||||
@ -76,9 +82,6 @@ android {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||
dimension = "default"
|
||||
}
|
||||
create("fdroid") {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resConfigs("en", "xxhdpi")
|
||||
dimension = "default"
|
||||
@ -117,34 +120,34 @@ android {
|
||||
dependencies {
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
implementation("org.tachiyomi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.3.2")
|
||||
implementation("androidx.core:core-ktx:1.6.0-beta01")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
val lifecycleVersion = "2.3.0"
|
||||
val lifecycleVersion = "2.4.0-alpha01"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.3.0")
|
||||
implementation("com.google.android.material:material:1.4.0-beta01")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
|
||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
@ -153,7 +156,7 @@ dependencies {
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "5.0.0-alpha.2"
|
||||
val okhttpVersion = "4.9.1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
@ -163,7 +166,7 @@ dependencies {
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.1.0"
|
||||
val kotlinSerializationVersion = "1.2.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
@ -184,10 +187,10 @@ dependencies {
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
implementation("io.requery:sqlite-android:3.33.0")
|
||||
implementation("com.github.requery:sqlite-android:3.35.5")
|
||||
|
||||
// Preferences
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||
|
||||
// Model View Presenter
|
||||
val nucleusVersion = "3.0.0"
|
||||
@ -198,31 +201,32 @@ dependencies {
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
val glideVersion = "4.12.0"
|
||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||
val coilVersion = "1.2.1"
|
||||
implementation("io.coil-kt:coil:$coilVersion")
|
||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
|
||||
// Crash reports
|
||||
implementation("ch.acra:acra-http:5.7.0")
|
||||
implementation("ch.acra:acra-http:5.8.1")
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
// UI
|
||||
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
|
||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||
|
||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||
val materialDialogsVersion = "3.1.1"
|
||||
@ -238,7 +242,7 @@ dependencies {
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
||||
|
||||
// FlowBinding
|
||||
val flowbindingVersion = "0.12.0"
|
||||
val flowbindingVersion = "1.0.0"
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
@ -278,7 +282,8 @@ tasks {
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
||||
)
|
||||
}
|
||||
|
||||
|
34
app/proguard-android-optimize.txt
Normal file
34
app/proguard-android-optimize.txt
Normal file
@ -0,0 +1,34 @@
|
||||
-allowaccessmodification
|
||||
-dontusemixedcaseclassnames
|
||||
-verbose
|
||||
|
||||
-keepattributes *Annotation*
|
||||
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
-keep @androidx.annotation.Keep class * {*;}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <init>(...);
|
||||
}
|
68
app/proguard-rules.pro
vendored
68
app/proguard-rules.pro
vendored
@ -1,29 +1,19 @@
|
||||
-dontobfuscate
|
||||
|
||||
# Extensions may require methods unused in the core app
|
||||
-dontwarn eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.** { public protected private *; }
|
||||
# Keep extension's common dependencies
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||
-keep,allowoptimization class androidx.preference.** { *; }
|
||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||
-keep,allowoptimization class okio.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
|
||||
-keep class org.jsoup.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||
-keep class com.squareup.duktape.** { *; }
|
||||
|
||||
# Design library
|
||||
-dontwarn com.google.android.material.**
|
||||
-keep class com.google.android.material.** { *; }
|
||||
-keep interface com.google.android.material.** { *; }
|
||||
-keep public class com.google.android.material.R$* { *; }
|
||||
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||
<init>();
|
||||
}
|
||||
|
||||
# RxJava 1.1.0
|
||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||
@ -39,30 +29,38 @@
|
||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||
}
|
||||
|
||||
# ReactiveNetwork
|
||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||
|
||||
## GSON ##
|
||||
-dontnote rx.internal.util.PlatformDependent
|
||||
##---------------End: proguard configuration for RxJava 1.x ----------
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# Gson specific classes
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Prevent R8 from leaving Data object members always null
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
## kotlinx.serialization ##
|
||||
|
||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
|
||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
*** Companion;
|
||||
}
|
||||
@ -77,3 +75,9 @@
|
||||
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep class kotlinx.serialization.**
|
||||
-keepclassmembers class kotlinx.serialization.** {
|
||||
<methods>;
|
||||
}
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||
|
@ -17,7 +17,7 @@
|
||||
android:shortcutDisabledMessage="@string/app_not_available"
|
||||
android:shortcutId="show_recently_updated"
|
||||
android:shortcutLongLabel="@string/label_recent_updates"
|
||||
android:shortcutShortLabel="@string/short_recent_updates">
|
||||
android:shortcutShortLabel="@string/label_recent_updates">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
|
@ -1,34 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108.0"
|
||||
android:viewportHeight="108.0">
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108.0"
|
||||
android:viewportHeight="108.0">
|
||||
<path
|
||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#000"/>
|
||||
<path
|
||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#455A64"/>
|
||||
<path
|
||||
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#607D8B"/>
|
||||
<path
|
||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#000"/>
|
||||
<path
|
||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#CE2828"/>
|
||||
<path
|
||||
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#FFF"/>
|
||||
<path
|
||||
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#000"/>
|
||||
</vector>
|
||||
|
@ -83,7 +83,7 @@
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.security.BiometricUnlockActivity"
|
||||
android:name=".ui.security.UnlockActivity"
|
||||
android:theme="@style/Theme.Base" />
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
|
@ -1,40 +1,53 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.multidex.MultiDex
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import org.acra.ACRA
|
||||
import org.acra.annotation.AcraCore
|
||||
import org.acra.annotation.AcraHttpSender
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.acra.config.httpSender
|
||||
import org.acra.ktx.initAcra
|
||||
import org.acra.sender.HttpSender
|
||||
import org.conscrypt.Conscrypt
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.Security
|
||||
|
||||
@AcraCore(
|
||||
buildConfigClass = BuildConfig::class,
|
||||
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
|
||||
)
|
||||
@AcraHttpSender(
|
||||
uri = BuildConfig.ACRA_URI,
|
||||
httpMethod = HttpSender.Method.PUT
|
||||
)
|
||||
open class App : Application(), LifecycleObserver {
|
||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
@ -44,6 +57,12 @@ open class App : Application(), LifecycleObserver {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
// Avoid potential crashes
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||
}
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupAcra()
|
||||
@ -52,6 +71,34 @@ open class App : Application(), LifecycleObserver {
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
// Show notification to disable Incognito Mode when it's enabled
|
||||
preferences.incognitoMode().asFlow()
|
||||
.onEach { enabled ->
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
if (enabled) {
|
||||
disableIncognitoReceiver.register()
|
||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||
setContentText(getString(R.string.notification_incognito_text))
|
||||
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
||||
setOngoing(true)
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
this@App,
|
||||
0,
|
||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
)
|
||||
setContentIntent(pendingIntent)
|
||||
}
|
||||
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
||||
} else {
|
||||
disableIncognitoReceiver.unregister()
|
||||
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
||||
}
|
||||
}
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@ -64,6 +111,23 @@ open class App : Application(), LifecycleObserver {
|
||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
componentRegistry {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder(this@App))
|
||||
} else {
|
||||
add(GifDecoder())
|
||||
}
|
||||
add(ByteBufferFetcher())
|
||||
add(MangaCoverFetcher())
|
||||
}
|
||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||
crossfade(300)
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
@Suppress("unused")
|
||||
fun onAppBackgrounded() {
|
||||
@ -74,11 +138,45 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
protected open fun setupAcra() {
|
||||
if (BuildConfig.FLAVOR != "dev") {
|
||||
ACRA.init(this)
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||
|
||||
httpSender {
|
||||
uri = BuildConfig.ACRA_URI
|
||||
httpMethod = HttpSender.Method.PUT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun setupNotificationChannels() {
|
||||
Notifications.createChannels(this)
|
||||
}
|
||||
|
||||
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
||||
private var registered = false
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
preferences.incognitoMode().set(false)
|
||||
}
|
||||
|
||||
fun register() {
|
||||
if (!registered) {
|
||||
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
if (registered) {
|
||||
unregisterReceiver(this)
|
||||
registered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@ -42,8 +41,6 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -139,6 +141,55 @@ object Migrations {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 59) {
|
||||
// Reset rotation to Free after replacing Lock
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (prefs.contains("pref_rotation_type_key")) {
|
||||
prefs.edit {
|
||||
putInt("pref_rotation_type_key", 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Disable update check for Android 5.x users
|
||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
UpdaterJob.cancelTask(context)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 60) {
|
||||
// Re-enable update check that was prevously accidentally disabled for M
|
||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
||||
UpdaterJob.setupTask(context)
|
||||
}
|
||||
|
||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||
1 -> OrientationType.FREE.flagValue
|
||||
2 -> OrientationType.PORTRAIT.flagValue
|
||||
3 -> OrientationType.LANDSCAPE.flagValue
|
||||
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
|
||||
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
|
||||
else -> OrientationType.FREE.flagValue
|
||||
}
|
||||
|
||||
// Reading mode flag and prefValue is the same value
|
||||
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
||||
|
||||
prefs.edit {
|
||||
putInt("pref_default_orientation_type_key", newOrientation)
|
||||
remove("pref_rotation_type_key")
|
||||
putInt("pref_default_reading_mode_key", newReadingMode)
|
||||
remove("pref_default_viewer_key")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 61) {
|
||||
// Handle removed every 1 or 2 hour library updates
|
||||
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||
if (updateInterval == 1 || updateInterval == 2) {
|
||||
preferences.libraryUpdateInterval().set(3)
|
||||
LibraryUpdateJob.setupTask(context, 3)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ object BackupConst {
|
||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
||||
|
||||
const val BACKUP_TYPE_LEGACY = 0
|
||||
const val BACKUP_TYPE_FULL = 1
|
||||
|
@ -10,7 +10,6 @@ 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.backup.legacy.LegacyBackupManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
@ -48,12 +47,11 @@ class BackupCreateService : Service() {
|
||||
* @param uri path of Uri
|
||||
* @param flags determines what to backup
|
||||
*/
|
||||
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
||||
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)
|
||||
putExtra(BackupConst.EXTRA_TYPE, type)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
@ -101,17 +99,11 @@ class BackupCreateService : Service() {
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
|
||||
try {
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
||||
val backupManager = when (backupType) {
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
|
||||
else -> LegacyBackupManager(this)
|
||||
}
|
||||
|
||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||
notifier.showBackupComplete(unifile)
|
||||
} catch (e: Exception) {
|
||||
notifier.showBackupError(e.message)
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -23,9 +22,6 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
return try {
|
||||
FullBackupManager(context).createBackup(uri, flags, true)
|
||||
if (preferences.createLegacyBackup().get()) {
|
||||
LegacyBackupManager(context).createBackup(uri, flags, true)
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
|
@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
||||
fun showBackupComplete(unifile: UniFile) {
|
||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
|
@ -25,7 +25,7 @@ data class BackupManga(
|
||||
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||
@ProtoNumber(14) var viewer: Int = 0,
|
||||
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
||||
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||
@ -34,6 +34,7 @@ data class BackupManga(
|
||||
@ProtoNumber(100) var favorite: Boolean = true,
|
||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
@ -48,7 +49,7 @@ data class BackupManga(
|
||||
favorite = this@BackupManga.favorite
|
||||
source = this@BackupManga.source
|
||||
date_added = this@BackupManga.dateAdded
|
||||
viewer = this@BackupManga.viewer
|
||||
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
||||
chapter_flags = this@BackupManga.chapterFlags
|
||||
}
|
||||
}
|
||||
@ -79,7 +80,8 @@ data class BackupManga(
|
||||
favorite = manga.favorite,
|
||||
source = manga.source,
|
||||
dateAdded = manga.date_added,
|
||||
viewer = manga.viewer,
|
||||
viewer = manga.readingModeType,
|
||||
viewer_flags = manga.viewer_flags,
|
||||
chapterFlags = manga.chapter_flags
|
||||
)
|
||||
}
|
||||
|
@ -5,30 +5,11 @@ import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||
import com.github.salomonbrys.kotson.set
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||
@ -45,10 +26,8 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||
@ -70,161 +49,8 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
* @param uri path of Uri
|
||||
* @param isJob backup called from job
|
||||
*/
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
// Create manga array
|
||||
val mangaEntries = JsonArray()
|
||||
|
||||
// Create category array
|
||||
val categoryEntries = JsonArray()
|
||||
|
||||
// Create extension ID/name mapping
|
||||
val extensionEntries = JsonArray()
|
||||
|
||||
// Add value's to root
|
||||
root[Backup.VERSION] = CURRENT_VERSION
|
||||
root[Backup.MANGAS] = mangaEntries
|
||||
root[CATEGORIES] = categoryEntries
|
||||
root[EXTENSIONS] = extensionEntries
|
||||
|
||||
databaseHelper.inTransaction {
|
||||
val mangas = getFavoriteManga()
|
||||
|
||||
val extensions: MutableSet<String> = mutableSetOf()
|
||||
|
||||
// Backup library manga and its dependencies
|
||||
mangas.forEach { manga ->
|
||||
mangaEntries.add(backupMangaObject(manga, flags))
|
||||
|
||||
// Maintain set of extensions/sources used (excludes local source)
|
||||
if (manga.source != LocalSource.ID) {
|
||||
sourceManager.get(manga.source)?.let {
|
||||
extensions.add("${manga.source}:${it.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup categories
|
||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||
backupCategories(categoryEntries)
|
||||
}
|
||||
|
||||
// Backup extension ID/name mapping
|
||||
backupExtensionInfo(extensionEntries, extensions)
|
||||
}
|
||||
|
||||
try {
|
||||
val file: UniFile = (
|
||||
if (isJob) {
|
||||
// Get dir of file and create
|
||||
var dir = UniFile.fromUri(context, uri)
|
||||
dir = dir.createDirectory("automatic")
|
||||
|
||||
// Delete older backups
|
||||
val numberOfBackups = numberOfBackups()
|
||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||
.orEmpty()
|
||||
.sortedByDescending { it.name }
|
||||
.drop(numberOfBackups - 1)
|
||||
.forEach { it.delete() }
|
||||
|
||||
// Create new file to place backup
|
||||
dir.createFile(Backup.getDefaultFilename())
|
||||
} else {
|
||||
UniFile.fromUri(context, uri)
|
||||
}
|
||||
)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
|
||||
file.openOutputStream().bufferedWriter().use {
|
||||
parser.toJson(root, it)
|
||||
}
|
||||
return file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||
extensions.sorted().forEach {
|
||||
root.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup the categories of library
|
||||
*
|
||||
* @param root root of categories json
|
||||
*/
|
||||
internal fun backupCategories(root: JsonArray) {
|
||||
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a manga to Json
|
||||
*
|
||||
* @param manga manga that gets converted
|
||||
* @return [JsonElement] containing manga information
|
||||
*/
|
||||
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||
// Entry for this manga
|
||||
val entry = JsonObject()
|
||||
|
||||
// Backup manga fields
|
||||
entry[MANGA] = parser.toJsonTree(manga)
|
||||
|
||||
// Check if user wants chapter information in backup
|
||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chaptersJson = parser.toJsonTree(chapters)
|
||||
if (chaptersJson.asJsonArray.size() > 0) {
|
||||
entry[CHAPTERS] = chaptersJson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
val categoriesNames = categoriesForManga.map { it.name }
|
||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
if (tracks.isNotEmpty()) {
|
||||
entry[TRACK] = parser.toJsonTree(tracks)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||
if (historyForManga.isNotEmpty()) {
|
||||
val historyData = historyForManga.mapNotNull { history ->
|
||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||
url?.let { DHistory(url, history.last_read) }
|
||||
}
|
||||
val historyJson = parser.toJsonTree(historyData)
|
||||
if (historyJson.asJsonArray.size() > 0) {
|
||||
entry[HISTORY] = historyJson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
||||
throw IllegalStateException("Legacy backup creation is not supported")
|
||||
|
||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||
manga.id = dbManga.id
|
||||
|
@ -16,7 +16,7 @@ object MangaTypeAdapter {
|
||||
value(it.url)
|
||||
value(it.title)
|
||||
value(it.source)
|
||||
value(it.viewer)
|
||||
value(it.viewer_flags)
|
||||
value(it.chapter_flags)
|
||||
endArray()
|
||||
}
|
||||
@ -27,7 +27,7 @@ object MangaTypeAdapter {
|
||||
manga.url = nextString()
|
||||
manga.title = nextString()
|
||||
manga.source = nextLong()
|
||||
manga.viewer = nextInt()
|
||||
manga.viewer_flags = nextInt()
|
||||
manga.chapter_flags = nextInt()
|
||||
endArray()
|
||||
manga
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import coil.imageLoader
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import java.io.File
|
||||
@ -99,6 +100,13 @@ class CoverCache(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear coil's memory cache.
|
||||
*/
|
||||
fun clearMemoryCache() {
|
||||
context.imageLoader.memoryCache.clear()
|
||||
}
|
||||
|
||||
private fun getCacheDir(dir: String): File {
|
||||
return context.getExternalFilesDir(dir)
|
||||
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||
|
@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
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.network.HttpException
|
||||
import coil.request.get
|
||||
import coil.size.Size
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||
*
|
||||
* Available request parameter:
|
||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||
*/
|
||||
class MangaCoverFetcher : Fetcher<Manga> {
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
||||
|
||||
override fun key(data: Manga): String? {
|
||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
||||
return data.thumbnail_url!!
|
||||
}
|
||||
|
||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
||||
// Use custom cover if exists
|
||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
||||
if (useCustomCover && customCoverFile.exists()) {
|
||||
return fileLoader(customCoverFile)
|
||||
}
|
||||
|
||||
val cover = data.thumbnail_url
|
||||
return when (getResourceType(cover)) {
|
||||
Type.URL -> httpLoader(data, options)
|
||||
Type.File -> fileLoader(data)
|
||||
null -> error("Invalid image")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||
|
||||
// Use previously cached cover if exist
|
||||
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
||||
if (!manga.favorite) {
|
||||
coverFile.setLastModified(Date().time)
|
||||
}
|
||||
return fileLoader(coverFile)
|
||||
}
|
||||
|
||||
val (response, body) = awaitGetCall(manga, options)
|
||||
if (!response.isSuccessful) {
|
||||
body.close()
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
// Write to disk for future use
|
||||
if (options.diskCachePolicy.writeEnabled) {
|
||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
||||
tmpFile.parentFile?.mkdirs()
|
||||
tmpFile.sink().buffer().use { output ->
|
||||
output.writeAll(input)
|
||||
}
|
||||
if (coverFile.exists()) {
|
||||
coverFile.delete()
|
||||
}
|
||||
tmpFile.renameTo(coverFile)
|
||||
}
|
||||
}
|
||||
|
||||
return SourceResult(
|
||||
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> {
|
||||
val call = getCall(manga, options)
|
||||
val response = call.await()
|
||||
return response to checkNotNull(response.body) { "Null response source" }
|
||||
}
|
||||
|
||||
private fun getCall(manga: Manga, options: Options): Call {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
val client = source?.client ?: defaultClient
|
||||
|
||||
val newClient = client.newBuilder().build()
|
||||
|
||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||
if (source != null) {
|
||||
it.headers(source.headers)
|
||||
}
|
||||
|
||||
val networkRead = options.networkCachePolicy.readEnabled
|
||||
val diskRead = options.diskCachePolicy.readEnabled
|
||||
when {
|
||||
!networkRead && diskRead -> {
|
||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
||||
}
|
||||
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()
|
||||
|
||||
return newClient.newCall(request)
|
||||
}
|
||||
|
||||
private fun fileLoader(manga: Manga): FetchResult {
|
||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
||||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = file.source().buffer(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private fun getResourceType(cover: String?): Type? {
|
||||
return when {
|
||||
cover.isNullOrEmpty() -> null
|
||||
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Type {
|
||||
File, URL
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||
|
||||
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
COL_FAVORITE to obj.favorite,
|
||||
COL_LAST_UPDATE to obj.last_update,
|
||||
COL_INITIALIZED to obj.initialized,
|
||||
COL_VIEWER to obj.viewer,
|
||||
COL_VIEWER to obj.viewer_flags,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||
COL_DATE_ADDED to obj.date_added
|
||||
@ -85,7 +85,7 @@ interface BaseMangaGetResolver {
|
||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
|
||||
interface Manga : SManga {
|
||||
@ -15,78 +17,90 @@ interface Manga : SManga {
|
||||
|
||||
var date_added: Long
|
||||
|
||||
var viewer: Int
|
||||
var viewer_flags: Int
|
||||
|
||||
var chapter_flags: Int
|
||||
|
||||
var cover_last_modified: Long
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setFlags(order, SORT_MASK)
|
||||
setChapterFlags(order, CHAPTER_SORT_MASK)
|
||||
}
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
return chapter_flags and SORT_MASK == SORT_DESC
|
||||
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||
}
|
||||
|
||||
fun getGenres(): List<String>? {
|
||||
return genre?.split(", ")?.map { it.trim() }
|
||||
}
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
private fun setViewerFlags(flag: Int, mask: Int) {
|
||||
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
// Used to display the chapter's title one way or another
|
||||
var displayMode: Int
|
||||
get() = chapter_flags and DISPLAY_MASK
|
||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
||||
get() = chapter_flags and CHAPTER_DISPLAY_MASK
|
||||
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
|
||||
|
||||
var readFilter: Int
|
||||
get() = chapter_flags and READ_MASK
|
||||
set(filter) = setFlags(filter, READ_MASK)
|
||||
get() = chapter_flags and CHAPTER_READ_MASK
|
||||
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
|
||||
|
||||
var downloadedFilter: Int
|
||||
get() = chapter_flags and DOWNLOADED_MASK
|
||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
||||
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
|
||||
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
|
||||
|
||||
var bookmarkedFilter: Int
|
||||
get() = chapter_flags and BOOKMARKED_MASK
|
||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
||||
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
|
||||
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
|
||||
|
||||
var sorting: Int
|
||||
get() = chapter_flags and SORTING_MASK
|
||||
set(sort) = setFlags(sort, SORTING_MASK)
|
||||
get() = chapter_flags and CHAPTER_SORTING_MASK
|
||||
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
|
||||
|
||||
var readingModeType: Int
|
||||
get() = viewer_flags and ReadingModeType.MASK
|
||||
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
|
||||
|
||||
var orientationType: Int
|
||||
get() = viewer_flags and OrientationType.MASK
|
||||
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
||||
|
||||
companion object {
|
||||
|
||||
const val SORT_DESC = 0x00000000
|
||||
const val SORT_ASC = 0x00000001
|
||||
const val SORT_MASK = 0x00000001
|
||||
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000
|
||||
|
||||
const val SHOW_UNREAD = 0x00000002
|
||||
const val SHOW_READ = 0x00000004
|
||||
const val READ_MASK = 0x00000006
|
||||
const val CHAPTER_SORT_DESC = 0x00000000
|
||||
const val CHAPTER_SORT_ASC = 0x00000001
|
||||
const val CHAPTER_SORT_MASK = 0x00000001
|
||||
|
||||
const val SHOW_DOWNLOADED = 0x00000008
|
||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
||||
const val DOWNLOADED_MASK = 0x00000018
|
||||
const val CHAPTER_SHOW_UNREAD = 0x00000002
|
||||
const val CHAPTER_SHOW_READ = 0x00000004
|
||||
const val CHAPTER_READ_MASK = 0x00000006
|
||||
|
||||
const val SHOW_BOOKMARKED = 0x00000020
|
||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
||||
const val BOOKMARKED_MASK = 0x00000060
|
||||
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
|
||||
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
|
||||
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
|
||||
|
||||
const val SORTING_SOURCE = 0x00000000
|
||||
const val SORTING_NUMBER = 0x00000100
|
||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
||||
const val SORTING_MASK = 0x00000300
|
||||
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
|
||||
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
|
||||
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
|
||||
|
||||
const val DISPLAY_NAME = 0x00000000
|
||||
const val DISPLAY_NUMBER = 0x00100000
|
||||
const val DISPLAY_MASK = 0x00100000
|
||||
const val CHAPTER_SORTING_SOURCE = 0x00000000
|
||||
const val CHAPTER_SORTING_NUMBER = 0x00000100
|
||||
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
|
||||
const val CHAPTER_SORTING_MASK = 0x00000300
|
||||
|
||||
const val CHAPTER_DISPLAY_NAME = 0x00000000
|
||||
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
||||
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
||||
|
||||
fun create(source: Long): Manga = MangaImpl().apply {
|
||||
this.source = source
|
||||
|
@ -30,7 +30,7 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var viewer: Int = 0
|
||||
override var viewer_flags: Int = 0
|
||||
|
||||
override var chapter_flags: Int = 0
|
||||
|
||||
|
@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
@ -78,14 +77,24 @@ interface MangaQueries : DbProvider {
|
||||
|
||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun updateFlags(manga: Manga) = db.put()
|
||||
fun updateChapterFlags(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver())
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||
.prepare()
|
||||
|
||||
fun updateFlags(mangas: List<Manga>) = db.put()
|
||||
.objects(mangas)
|
||||
.withPutResolver(MangaFlagsPutResolver(true))
|
||||
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||
.objects(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
|
||||
.prepare()
|
||||
|
||||
fun updateViewerFlags(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||
.prepare()
|
||||
|
||||
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||
.objects(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||
.prepare()
|
||||
|
||||
fun updateLastUpdated(manga: Manga) = db.put()
|
||||
@ -98,11 +107,6 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaFavoritePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaViewer(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaViewerPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaTitle(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
|
@ -8,8 +8,9 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
@ -37,6 +38,6 @@ class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolve
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
||||
colName to fieldGetter.get(manga)
|
||||
)
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
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.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_VIEWER to manga.viewer
|
||||
)
|
||||
}
|
@ -203,6 +203,15 @@ class DownloadManager(private val context: Context) {
|
||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||
}
|
||||
|
||||
fun deletePendingDownloads(vararg downloads: Download) {
|
||||
val downloadsByManga = downloads.groupBy { it.manga.id }
|
||||
downloadsByManga.map { entry ->
|
||||
val manga = entry.value.first().manga
|
||||
val source = entry.value.first().source
|
||||
deleteChapters(entry.value.map { it.chapter }, manga, source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directories of a list of downloaded chapters.
|
||||
*
|
||||
|
@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
||||
val sourceDir = findSourceDir(source)
|
||||
return sourceDir?.findFile(getMangaDirName(manga))
|
||||
return sourceDir?.findFile(getMangaDirName(manga), true)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||
val mangaDir = findMangaDir(manga, source)
|
||||
return getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it) }
|
||||
.mapNotNull { mangaDir?.findFile(it, true) }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun getSourceDirName(source: Source): String {
|
||||
return source.toString()
|
||||
return DiskUtil.buildValidFilename(source.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,6 +150,7 @@ class DownloadProvider(private val context: Context) {
|
||||
return listOf(
|
||||
getChapterDirName(chapter),
|
||||
|
||||
// TODO: remove this
|
||||
// Legacy chapter directory name used in v0.9.2 and before
|
||||
DiskUtil.buildValidFilename(chapter.name)
|
||||
)
|
||||
|
@ -1,60 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.ContentValues.TAG
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(File(filePath), callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
try {
|
||||
data = FileInputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Timber.d(e, "Failed to open file")
|
||||
}
|
||||
callback.onLoadFailed(e)
|
||||
return
|
||||
}
|
||||
|
||||
callback.onDataReady(data)
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
data?.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
|
||||
open class LibraryMangaCustomCoverFetcher(
|
||||
private val manga: Manga,
|
||||
private val coverCache: CoverCache
|
||||
) : FileFetcher() {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
getCustomCoverFile()?.let {
|
||||
loadFromFile(it, callback)
|
||||
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
|
||||
}
|
||||
|
||||
protected fun getCustomCoverFile(): File? {
|
||||
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [DataFetcher] for loading a cover of a library manga.
|
||||
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
||||
* and copies the result to the cache.
|
||||
*
|
||||
* @param networkFetcher the network fetcher for this cover.
|
||||
* @param manga the manga of the cover to load.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
*/
|
||||
class LibraryMangaUrlFetcher(
|
||||
private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val coverCache: CoverCache
|
||||
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
getCustomCoverFile()?.let {
|
||||
loadFromFile(it, callback)
|
||||
return
|
||||
}
|
||||
|
||||
val cover = coverCache.getCoverFile(manga)
|
||||
if (cover == null) {
|
||||
callback.onLoadFailed(Exception("Null thumbnail url"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!cover.exists()) {
|
||||
networkFetcher.loadData(
|
||||
priority,
|
||||
object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(cover.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile!!.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(cover)
|
||||
loadFromFile(cover, callback)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
} else {
|
||||
callback.onLoadFailed(Exception("Null data"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
loadFromFile(cover, callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
super.cancel()
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
||||
val key = manga.url + coverLastModified
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
||||
|
||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
@ -1,134 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.Headers
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
||||
*
|
||||
* - Check in RAM LRU.
|
||||
* - Check in disk LRU.
|
||||
* - Check in this module.
|
||||
* - Fetch from the network connection.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
*/
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Default network client.
|
||||
*/
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
*/
|
||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
||||
|
||||
/**
|
||||
* Factory class for creating [MangaThumbnailModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
|
||||
return MangaThumbnailModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
override fun handles(model: MangaThumbnail): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
* @param mangaThumbnail the model.
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
override fun buildLoadData(
|
||||
mangaThumbnail: MangaThumbnail,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
val manga = mangaThumbnail.manga
|
||||
val url = manga.thumbnail_url
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
return if (!manga.favorite || manga.isLocal()) {
|
||||
null
|
||||
} else {
|
||||
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
||||
}
|
||||
}
|
||||
|
||||
if (url.startsWith("http", true)) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||
|
||||
// Get the resource fetcher for this request url.
|
||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
||||
|
||||
if (!manga.favorite) {
|
||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||
}
|
||||
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
||||
} else {
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
||||
*
|
||||
* @param manga the model.
|
||||
*/
|
||||
private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||
if (source == null) return LazyHeaders.DEFAULT
|
||||
|
||||
return cachedHeaders.getOrPut(manga.source) {
|
||||
LazyHeaders.Builder().apply {
|
||||
val nullStr: String? = null
|
||||
setHeader("User-Agent", nullStr)
|
||||
for ((key, value) in source.headers.toMultimap()) {
|
||||
addHeader(key, value[0])
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||
|
||||
override fun buildLoadData(
|
||||
model: InputStream,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
||||
}
|
||||
|
||||
override fun handles(model: InputStream): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
stream.close()
|
||||
} catch (e: IOException) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun loadData(
|
||||
priority: Priority,
|
||||
callback: DataFetcher.DataCallback<in InputStream>
|
||||
) {
|
||||
callback.onDataReady(stream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory class for creating [PassthroughModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
||||
|
||||
override fun build(
|
||||
multiFactory: MultiModelLoaderFactory
|
||||
): ModelLoader<InputStream, InputStream> {
|
||||
return PassthroughModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
@GlideModule
|
||||
class TachiGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||
builder.setDefaultTransitionOptions(
|
||||
Drawable::class.java,
|
||||
DrawableTransitionOptions.withCrossFade()
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
networkFactory
|
||||
)
|
||||
registry.append(
|
||||
MangaThumbnail::class.java,
|
||||
InputStream::class.java,
|
||||
MangaThumbnailModelLoader.Factory()
|
||||
)
|
||||
registry.append(
|
||||
InputStream::class.java,
|
||||
InputStream::class.java,
|
||||
PassthroughModelLoader.Factory()
|
||||
)
|
||||
}
|
||||
}
|
@ -8,7 +8,9 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -31,9 +33,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.libraryUpdateRestriction()!!
|
||||
val acRestriction = "ac" in restrictions
|
||||
val wifiRestriction = if ("wifi" in restrictions) {
|
||||
val restrictions = preferences.libraryUpdateRestriction().get()
|
||||
val acRestriction = CHARGING in restrictions
|
||||
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
|
@ -6,19 +6,22 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
@ -75,7 +78,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(title)
|
||||
.setContentTitle(title.chop(40))
|
||||
.setContentText("($current/$total)")
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
)
|
||||
@ -165,14 +169,17 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
// Per-manga notification
|
||||
if (!preferences.hideNotificationContent()) {
|
||||
updates.forEach { (manga, chapters) ->
|
||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||
launchUI {
|
||||
updates.forEach { (manga, chapters) ->
|
||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||
private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||
val icon = getMangaIcon(manga)
|
||||
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||
setContentTitle(manga.title)
|
||||
|
||||
@ -182,7 +189,6 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
|
||||
val icon = getMangaIcon(manga)
|
||||
if (icon != null) {
|
||||
setLargeIcon(icon)
|
||||
}
|
||||
@ -226,23 +232,14 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||
}
|
||||
|
||||
private fun getMangaIcon(manga: Manga): Bitmap? {
|
||||
return try {
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(manga.toMangaThumbnail())
|
||||
.dontTransform()
|
||||
.centerCrop()
|
||||
.circleCrop()
|
||||
.override(
|
||||
NOTIF_ICON_SIZE,
|
||||
NOTIF_ICON_SIZE
|
||||
)
|
||||
.submit()
|
||||
.get()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
private suspend fun getMangaIcon(manga: Manga): Bitmap? {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(manga)
|
||||
.transformations(CircleCropTransformation())
|
||||
.size(NOTIF_ICON_SIZE)
|
||||
.build()
|
||||
val drawable = context.imageLoader.execute(request).drawable
|
||||
return (drawable as? BitmapDrawable)?.bitmap
|
||||
}
|
||||
|
||||
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
||||
|
@ -21,12 +21,15 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
@ -271,6 +274,7 @@ class LibraryUpdateService(
|
||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
var hasDownloads = false
|
||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
@ -299,6 +303,10 @@ class LibraryUpdateService(
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
@ -386,6 +394,7 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
coverCache.clearMemoryCache()
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
@ -406,27 +415,35 @@ class LibraryUpdateService(
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
|
||||
// Update the tracking details.
|
||||
db.getTracks(manga).executeAsBlocking()
|
||||
.map { track ->
|
||||
supervisorScope {
|
||||
async {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
private suspend fun updateTrackings(manga: LibraryManga, loggedServices: List<TrackService>) {
|
||||
db.getTracks(manga).executeAsBlocking()
|
||||
.map { track ->
|
||||
supervisorScope {
|
||||
async {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
|
||||
if (service is UnattendedTrackService) {
|
||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,22 +58,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_SHARE_IMAGE ->
|
||||
shareImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// Delete image from path and dismiss notification
|
||||
ACTION_DELETE_IMAGE ->
|
||||
deleteImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// Share backup file
|
||||
ACTION_SHARE_BACKUP ->
|
||||
shareFile(
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
|
||||
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||
"application/x-protobuf+gzip",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||
@ -106,7 +106,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_SHARE_CRASH_LOG ->
|
||||
shareFile(
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||
"text/plain",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
@ -281,7 +281,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
|
||||
|
||||
/**
|
||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||
@ -494,11 +493,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_BACKUP
|
||||
putExtra(EXTRA_URI, uri)
|
||||
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
@ -68,6 +68,12 @@ object Notifications {
|
||||
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
||||
const val ID_CRASH_LOGS = -601
|
||||
|
||||
/**
|
||||
* Notification channel used for Incognito Mode
|
||||
*/
|
||||
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
||||
const val ID_INCOGNITO_MODE = -701
|
||||
|
||||
private val deprecatedChannels = listOf(
|
||||
"downloader_channel",
|
||||
"backup_restore_complete_channel"
|
||||
@ -154,6 +160,11 @@ object Notifications {
|
||||
CHANNEL_CRASH_LOGS,
|
||||
context.getString(R.string.channel_crash_logs),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_INCOGNITO_MODE,
|
||||
context.getString(R.string.pref_incognito_mode),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
).forEach(context.notificationManager::createNotificationChannel)
|
||||
|
||||
|
@ -13,9 +13,9 @@ object PreferenceKeys {
|
||||
|
||||
const val confirmExit = "pref_confirm_exit"
|
||||
|
||||
const val hideBottomBar = "pref_hide_bottom_bar_on_scroll"
|
||||
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
||||
|
||||
const val rotation = "pref_rotation_type_key"
|
||||
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom"
|
||||
|
||||
const val enableTransitions = "pref_enable_transitions_key"
|
||||
|
||||
@ -51,7 +51,11 @@ object PreferenceKeys {
|
||||
|
||||
const val colorFilterMode = "color_filter_mode"
|
||||
|
||||
const val defaultViewer = "pref_default_viewer_key"
|
||||
const val grayscale = "pref_grayscale"
|
||||
|
||||
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||
|
||||
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||
|
||||
const val imageScaleType = "pref_image_scale_type_key"
|
||||
|
||||
@ -95,6 +99,8 @@ object PreferenceKeys {
|
||||
|
||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||
|
||||
const val autoAddTrack = "pref_auto_add_track_key"
|
||||
|
||||
const val lastUsedSource = "last_catalogue_source"
|
||||
|
||||
const val lastUsedCategory = "last_used_category"
|
||||
@ -109,6 +115,8 @@ object PreferenceKeys {
|
||||
|
||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||
|
||||
const val folderPerManga = "create_folder_per_manga"
|
||||
|
||||
const val numberOfBackups = "backup_slots"
|
||||
|
||||
const val backupInterval = "backup_interval"
|
||||
@ -148,7 +156,7 @@ object PreferenceKeys {
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
const val useBiometricLock = "use_biometric_lock"
|
||||
const val useAuthenticator = "use_biometric_lock"
|
||||
|
||||
const val lockAppAfter = "lock_app_after"
|
||||
|
||||
@ -160,6 +168,8 @@ object PreferenceKeys {
|
||||
|
||||
const val autoUpdateMetadata = "auto_update_metadata"
|
||||
|
||||
const val autoUpdateTrackers = "auto_update_trackers"
|
||||
|
||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
||||
|
||||
const val downloadNew = "download_new"
|
||||
@ -183,6 +193,8 @@ object PreferenceKeys {
|
||||
|
||||
const val unreadBadge = "display_unread_badge"
|
||||
|
||||
const val localBadge = "display_local_badge"
|
||||
|
||||
const val categoryTabs = "display_category_tabs"
|
||||
|
||||
const val categoryNumberOfItems = "display_number_of_items"
|
||||
@ -207,8 +219,6 @@ object PreferenceKeys {
|
||||
|
||||
const val incognitoMode = "incognito_mode"
|
||||
|
||||
const val createLegacyBackup = "create_legacy_backup"
|
||||
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
|
||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
|
@ -1,5 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
const val UNMETERED_NETWORK = "wifi"
|
||||
const val CHARGING = "ac"
|
||||
|
||||
/**
|
||||
* This class stores the values for the preferences in the application.
|
||||
*/
|
||||
@ -18,13 +21,17 @@ object PreferenceValues {
|
||||
enum class LightThemeVariant {
|
||||
default,
|
||||
blue,
|
||||
strawberrydaiquiri,
|
||||
}
|
||||
|
||||
// Keys are lowercase to match legacy string values
|
||||
enum class DarkThemeVariant {
|
||||
default,
|
||||
blue,
|
||||
greenapple,
|
||||
midnightdusk,
|
||||
amoled,
|
||||
hotpink,
|
||||
}
|
||||
|
||||
/* ktlint-enable experimental:enum-entry-name-case */
|
||||
|
@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -36,8 +38,9 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||
set(get() - item)
|
||||
}
|
||||
|
||||
fun Preference<Boolean>.toggle() {
|
||||
fun Preference<Boolean>.toggle(): Boolean {
|
||||
set(!get())
|
||||
return get()
|
||||
}
|
||||
|
||||
class PreferencesHelper(val context: Context) {
|
||||
@ -61,9 +64,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
||||
|
||||
fun hideBottomBar() = flowPrefs.getBoolean(Keys.hideBottomBar, true)
|
||||
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
||||
|
||||
fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
|
||||
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false)
|
||||
|
||||
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
||||
|
||||
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
||||
|
||||
@ -75,9 +80,9 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
||||
|
||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||
|
||||
fun clear() = prefs.edit { clear() }
|
||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
||||
|
||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
||||
|
||||
@ -85,8 +90,6 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
||||
|
||||
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
||||
|
||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||
|
||||
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
|
||||
@ -121,7 +124,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
||||
|
||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
|
||||
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
||||
|
||||
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||
|
||||
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||
|
||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
||||
|
||||
@ -167,6 +174,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||
|
||||
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
||||
|
||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||
|
||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
||||
@ -203,6 +212,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||
|
||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||
|
||||
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
||||
|
||||
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
||||
@ -215,7 +226,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
||||
|
||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
||||
@ -226,6 +237,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||
|
||||
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
|
||||
|
||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
||||
|
||||
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
||||
@ -289,16 +302,14 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
|
||||
|
||||
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE)
|
||||
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.CHAPTER_SORTING_SOURCE)
|
||||
|
||||
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME)
|
||||
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.CHAPTER_DISPLAY_NAME)
|
||||
|
||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC)
|
||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
||||
|
||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||
|
||||
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
|
||||
|
||||
fun setChapterSettingsDefault(manga: Manga) {
|
||||
prefs.edit {
|
||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||
@ -306,7 +317,7 @@ class PreferencesHelper(val context: Context) {
|
||||
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
|
||||
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
|
||||
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
|
||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC)
|
||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
/**
|
||||
* A TrackService that doesn't need explicit login.
|
||||
*/
|
||||
interface NoLoginTrackService {
|
||||
fun loginNoop()
|
||||
}
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
|
||||
@ -15,6 +16,7 @@ class TrackManager(context: Context) {
|
||||
const val KITSU = 3
|
||||
const val SHIKIMORI = 4
|
||||
const val BANGUMI = 5
|
||||
const val KOMGA = 6
|
||||
}
|
||||
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
@ -27,7 +29,9 @@ class TrackManager(context: Context) {
|
||||
|
||||
val bangumi = Bangumi(context, BANGUMI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||
val komga = Komga(context, KOMGA)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
|
@ -46,8 +46,6 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
abstract suspend fun add(track: Track): Track
|
||||
|
||||
abstract suspend fun update(track: Track): Track
|
||||
|
||||
abstract suspend fun bind(track: Track): Track
|
||||
|
@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
/**
|
||||
* An Unattended Track Service will never prompt the user to match a manga with the remote.
|
||||
* It is expected that such Track Sercice can only work with specific sources and unique IDs.
|
||||
*/
|
||||
interface UnattendedTrackService {
|
||||
/**
|
||||
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||
*/
|
||||
fun accept(source: Source): Boolean
|
||||
|
||||
/**
|
||||
* match is similar to TrackService.search, but only return zero or one match.
|
||||
*/
|
||||
suspend fun match(manga: Manga): TrackSearch?
|
||||
}
|
@ -130,7 +130,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
@ -27,10 +28,14 @@ import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
private val authClient = client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.addInterceptor(RateLimitInterceptor(85, 1, MINUTES))
|
||||
.build()
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
|
@ -31,7 +31,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
return df.format(track.score)
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track, getUserId())
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,95 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import okhttp3.Dns
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class Komga(private val context: Context, id: Int) : TrackService(id), UnattendedTrackService, NoLoginTrackService {
|
||||
|
||||
companion object {
|
||||
const val UNREAD = 1
|
||||
const val READING = 2
|
||||
const val COMPLETED = 3
|
||||
|
||||
const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga"
|
||||
}
|
||||
|
||||
override val client: OkHttpClient =
|
||||
networkService.client.newBuilder()
|
||||
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
||||
.build()
|
||||
|
||||
val api by lazy { KomgaApi(client) }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_komga
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_komga
|
||||
|
||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||
|
||||
override fun getStatusList() = listOf(UNREAD, READING, COMPLETED)
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
UNREAD -> getString(R.string.unread)
|
||||
READING -> getString(R.string.currently_reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCompletionStatus(): Int = COMPLETED
|
||||
|
||||
override fun getScoreList(): List<String> = emptyList()
|
||||
|
||||
override fun displayScore(track: Track): String = ""
|
||||
|
||||
override suspend fun update(track: Track): Track {
|
||||
return api.updateProgress(track)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track): Track {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
TODO("Not yet implemented: search")
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getTrackSearch(track.tracking_url)!!
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String) {
|
||||
saveCredentials("user", "pass")
|
||||
}
|
||||
|
||||
// TrackService.isLogged works by checking that credentials are saved.
|
||||
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
||||
override fun loginNoop() {
|
||||
saveCredentials("user", "pass")
|
||||
}
|
||||
|
||||
override fun accept(source: Source): Boolean = source::class.qualifiedName == ACCEPTED_SOURCE
|
||||
|
||||
override suspend fun match(manga: Manga): TrackSearch? =
|
||||
try {
|
||||
api.getTrackSearch(manga.url)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
const val READLIST_API = "/api/v1/readlists"
|
||||
|
||||
class KomgaApi(private val client: OkHttpClient) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
suspend fun getTrackSearch(url: String): TrackSearch =
|
||||
withIOContext {
|
||||
try {
|
||||
val track = if (url.contains(READLIST_API)) {
|
||||
client.newCall(GET(url))
|
||||
.await()
|
||||
.parseAs<ReadListDto>()
|
||||
.toTrack()
|
||||
} else {
|
||||
client.newCall(GET(url))
|
||||
.await()
|
||||
.parseAs<SeriesDto>()
|
||||
.toTrack()
|
||||
}
|
||||
|
||||
val progress = client
|
||||
.newCall(GET("$url/read-progress/tachiyomi"))
|
||||
.await()
|
||||
.parseAs<ReadProgressDto>()
|
||||
|
||||
track.apply {
|
||||
cover_url = "$url/thumbnail"
|
||||
tracking_url = url
|
||||
total_chapters = progress.booksCount
|
||||
status = when (progress.booksCount) {
|
||||
progress.booksUnreadCount -> Komga.UNREAD
|
||||
progress.booksReadCount -> Komga.COMPLETED
|
||||
else -> Komga.READING
|
||||
}
|
||||
last_chapter_read = progress.lastReadContinuousIndex
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Could not get item: $url")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProgress(track: Track): Track {
|
||||
val progress = ReadProgressUpdateDto(track.last_chapter_read)
|
||||
val payload = json.encodeToString(progress)
|
||||
client.newCall(
|
||||
Request.Builder()
|
||||
.url("${track.tracking_url}/read-progress/tachiyomi")
|
||||
.put(payload.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
)
|
||||
.await()
|
||||
return getTrackSearch(track.tracking_url)
|
||||
}
|
||||
|
||||
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
||||
it.title = metadata.title
|
||||
it.summary = metadata.summary
|
||||
it.publishing_status = metadata.status
|
||||
}
|
||||
|
||||
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
||||
it.title = name
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SeriesDto(
|
||||
val id: String,
|
||||
val libraryId: String,
|
||||
val name: String,
|
||||
val created: String?,
|
||||
val lastModified: String?,
|
||||
val fileLastModified: String,
|
||||
val booksCount: Int,
|
||||
val booksReadCount: Int,
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val metadata: SeriesMetadataDto,
|
||||
val booksMetadata: BookMetadataAggregationDto
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeriesMetadataDto(
|
||||
val status: String,
|
||||
val created: String?,
|
||||
val lastModified: String?,
|
||||
val title: String,
|
||||
val titleSort: String,
|
||||
val summary: String,
|
||||
val summaryLock: Boolean,
|
||||
val readingDirection: String,
|
||||
val readingDirectionLock: Boolean,
|
||||
val publisher: String,
|
||||
val publisherLock: Boolean,
|
||||
val ageRating: Int?,
|
||||
val ageRatingLock: Boolean,
|
||||
val language: String,
|
||||
val languageLock: Boolean,
|
||||
val genres: Set<String>,
|
||||
val genresLock: Boolean,
|
||||
val tags: Set<String>,
|
||||
val tagsLock: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BookMetadataAggregationDto(
|
||||
val authors: List<AuthorDto> = emptyList(),
|
||||
val releaseDate: String?,
|
||||
val summary: String,
|
||||
val summaryNumber: String,
|
||||
|
||||
val created: String,
|
||||
val lastModified: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorDto(
|
||||
val name: String,
|
||||
val role: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadProgressUpdateDto(
|
||||
val lastBookRead: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadListDto(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val bookIds: List<String>,
|
||||
val createdDate: String,
|
||||
val lastModifiedDate: String,
|
||||
val filtered: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadProgressDto(
|
||||
val booksCount: Int,
|
||||
val booksReadCount: Int,
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val lastReadContinuousIndex: Int,
|
||||
)
|
@ -66,7 +66,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
return api.updateItem(track)
|
||||
|
@ -11,9 +11,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
@ -24,21 +21,19 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
if (oauth == null) {
|
||||
oauth = myanimelist.loadOAuth()
|
||||
}
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth!!.isExpired()) {
|
||||
// Refresh access token if expired
|
||||
if (oauth != null && oauth!!.isExpired()) {
|
||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
||||
if (it.isSuccessful) {
|
||||
setAuth(json.decodeFromString(it.body!!.string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("No authentication token")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
// Add the authorization header to the original request
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
@ -7,8 +7,9 @@ data class OAuth(
|
||||
val refresh_token: String,
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long = System.currentTimeMillis(),
|
||||
val expires_in: Long
|
||||
) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires_in
|
||||
fun isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000)
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import coil.util.CoilUtils
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@ -20,28 +23,32 @@ class NetworkHelper(context: Context) {
|
||||
|
||||
val cookieManager = AndroidCookieJar()
|
||||
|
||||
val client by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.cache(Cache(cacheDir, cacheSize))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
get() {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
|
||||
when (preferences.dohProvider()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
when (preferences.dohProvider()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
}
|
||||
val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||
|
||||
builder.build()
|
||||
}
|
||||
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
|
||||
|
||||
val cloudflareClient by lazy {
|
||||
client.newBuilder()
|
||||
|
@ -9,6 +9,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
@ -70,7 +71,9 @@ suspend fun Call.await(): Response {
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(response)
|
||||
continuation.resume(response) {
|
||||
response.body?.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
@ -114,10 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
// HTTP error codes are only received since M
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
url == origRequestUrl && !challengeFound
|
||||
) {
|
||||
if (url == origRequestUrl && !challengeFound) {
|
||||
// The first request didn't return the challenge, abort.
|
||||
latch.countDown()
|
||||
}
|
||||
@ -156,6 +153,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
// Throw exception if we failed to bypass Cloudflare
|
@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
||||
*
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
class RateLimitInterceptor(
|
||||
private val permits: Int,
|
||||
private val period: Long = 1,
|
||||
private val unit: TimeUnit = TimeUnit.SECONDS
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||
*
|
||||
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
class SpecificHostRateLimitInterceptor(
|
||||
private val httpUrl: HttpUrl,
|
||||
private val permits: Int,
|
||||
private val period: Long = 1,
|
||||
private val unit: TimeUnit = TimeUnit.SECONDS
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
private val host = httpUrl.host
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.request().url.host != host) {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.Interceptor
|
@ -32,8 +32,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
private const val COVER_NAME = "cover.jpg"
|
||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
@ -192,12 +190,10 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
ChapterRecognition.parseChapterNumber(this, manga)
|
||||
}
|
||||
}
|
||||
.sortedWith(
|
||||
Comparator { c1, c2 ->
|
||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}
|
||||
)
|
||||
.sortedWith { c1, c2 ->
|
||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}
|
||||
.toList()
|
||||
|
||||
return Observable.just(chapters)
|
||||
@ -242,7 +238,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
|
||||
private fun isSupportedFile(extension: String): Boolean {
|
||||
return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
||||
return extension.toLowerCase(Locale.ROOT) in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
@ -254,7 +250,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
|
||||
return getFormat(chapFile)
|
||||
}
|
||||
throw Exception("Chapter not found")
|
||||
throw Exception(context.getString(R.string.chapter_not_found))
|
||||
}
|
||||
|
||||
private fun getFormat(file: File): Format {
|
||||
@ -268,7 +264,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
} else if (extension.equals("epub", true)) {
|
||||
Format.Epub(file)
|
||||
} else {
|
||||
throw Exception("Invalid chapter format")
|
||||
throw Exception(context.getString(R.string.local_invalid_format))
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,9 +307,16 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
}
|
||||
|
||||
private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
|
||||
override fun getFilterList() = POPULAR_FILTERS
|
||||
|
||||
override fun getFilterList() = FilterList(OrderBy())
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||
|
||||
private class OrderBy(context: Context) : Filter.Sort(
|
||||
context.getString(R.string.local_filter_order_by),
|
||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||
Selection(0, true)
|
||||
)
|
||||
|
||||
sealed class Format {
|
||||
data class Directory(val file: File) : Format()
|
||||
|
@ -25,12 +25,16 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
when (preferences.themeDark().get()) {
|
||||
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
|
||||
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
|
||||
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Dark_Amoled
|
||||
DarkThemeVariant.greenapple -> R.style.Theme_Tachiyomi_Dark_GreenApple
|
||||
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
|
||||
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
||||
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Amoled_HotPink
|
||||
}
|
||||
} else {
|
||||
when (preferences.themeLight().get()) {
|
||||
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
|
||||
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
|
||||
LightThemeVariant.strawberrydaiquiri -> R.style.Theme_Tachiyomi_Light_StrawberryDaiquiri
|
||||
}
|
||||
}
|
||||
setTheme(themeId)
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
@ -21,11 +20,9 @@ fun Router.popControllerWithTag(tag: String): Boolean {
|
||||
|
||||
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
|
||||
val activity = activity ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
permissions.forEach { permission ->
|
||||
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(permission), requestCode)
|
||||
}
|
||||
permissions.forEach { permission ->
|
||||
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(permission), requestCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
@ -26,15 +27,17 @@ class OneWayFadeChangeHandler : FadeChangeHandler {
|
||||
isPush: Boolean,
|
||||
toAddedToContainer: Boolean
|
||||
): Animator {
|
||||
val animator = AnimatorSet()
|
||||
if (to != null) {
|
||||
return super.getAnimator(container, from, to, isPush, toAddedToContainer)
|
||||
val start: Float = if (toAddedToContainer) 0F else to.alpha
|
||||
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
|
||||
}
|
||||
|
||||
if (from != null && (!isPush || removesFromViewOnPush())) {
|
||||
container.removeView(from)
|
||||
from.alpha = 0f
|
||||
}
|
||||
|
||||
return AnimatorSet()
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler {
|
||||
|
@ -120,7 +120,7 @@ open class ExtensionController :
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.extension_main, menu)
|
||||
inflater.inflate(R.menu.browse_extensions, menu)
|
||||
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
@ -1,9 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.extension
|
||||
|
||||
import android.view.View
|
||||
import coil.clear
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@ -35,17 +36,15 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
binding.lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
||||
binding.warning.text = when {
|
||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
else -> ""
|
||||
}.toUpperCase()
|
||||
|
||||
GlideApp.with(itemView.context).clear(binding.image)
|
||||
binding.image.clear()
|
||||
if (extension is Extension.Available) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(extension.iconUrl)
|
||||
.into(binding.image)
|
||||
binding.image.load(extension.iconUrl)
|
||||
} else {
|
||||
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import androidx.preference.PreferenceManager
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||
import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@ -66,7 +65,10 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
||||
|
||||
val themedContext by lazy { getPreferenceThemeContext() }
|
||||
val manager = PreferenceManager(themedContext)
|
||||
manager.preferenceDataStore = EmptyPreferenceDataStore()
|
||||
val dataStore = SharedPreferencesDataStore(
|
||||
context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
)
|
||||
manager.preferenceDataStore = dataStore
|
||||
manager.onDisplayPreferenceDialogListener = this
|
||||
val screen = manager.createPreferenceScreen(themedContext)
|
||||
preferenceScreen = screen
|
||||
@ -101,10 +103,6 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
||||
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
|
||||
val context = screen.context
|
||||
|
||||
val dataStore = SharedPreferencesDataStore(
|
||||
context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
)
|
||||
|
||||
if (source is ConfigurableSource) {
|
||||
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
||||
source.setupPreferenceScreen(newScreen)
|
||||
@ -113,7 +111,6 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
||||
while (newScreen.preferenceCount != 0) {
|
||||
val pref = newScreen.getPreference(0)
|
||||
pref.isIconSpaceReserved = false
|
||||
pref.preferenceDataStore = dataStore
|
||||
pref.order = Int.MAX_VALUE // reset to default order
|
||||
|
||||
newScreen.removePreference(pref)
|
||||
|
@ -1,14 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||
|
||||
class MigrationMangaHolder(
|
||||
@ -28,15 +25,10 @@ class MigrationMangaHolder(
|
||||
binding.title.text = item.manga.title
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(itemView.context).clear(binding.thumbnail)
|
||||
|
||||
val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
|
||||
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
|
||||
GlideApp.with(itemView.context)
|
||||
.load(item.manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.apply(requestOptions)
|
||||
.dontAnimate()
|
||||
.into(binding.thumbnail)
|
||||
val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga) {
|
||||
transformations(RoundedCornersTransformation(radius))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import androidx.core.view.isVisible
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -15,6 +16,7 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SearchController(
|
||||
@ -69,12 +71,14 @@ class SearchController(
|
||||
super.onMangaClick(manga)
|
||||
}
|
||||
|
||||
fun renderIsReplacingManga(isReplacingManga: Boolean) {
|
||||
if (isReplacingManga) {
|
||||
binding.progress.isVisible = true
|
||||
} else {
|
||||
binding.progress.isVisible = false
|
||||
fun renderIsReplacingManga(isReplacingManga: Boolean, newManga: Manga?) {
|
||||
binding.progress.isVisible = isReplacingManga
|
||||
if (!isReplacingManga) {
|
||||
router.popController(this)
|
||||
if (newManga != null) {
|
||||
// Replaces old MangaController
|
||||
router.replaceTopController(RouterTransaction.with(MangaController(newManga)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,12 +26,16 @@ class SearchPresenter(
|
||||
private val manga: Manga
|
||||
) : GlobalSearchPresenter(initialQuery) {
|
||||
|
||||
private val replacingMangaRelay = BehaviorRelay.create<Boolean>()
|
||||
private val replacingMangaRelay = BehaviorRelay.create<Pair<Boolean, Manga?>>()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
replacingMangaRelay.subscribeLatestCache({ controller, isReplacingManga -> (controller as? SearchController)?.renderIsReplacingManga(isReplacingManga) })
|
||||
replacingMangaRelay.subscribeLatestCache(
|
||||
{ controller, (isReplacingManga, newManga) ->
|
||||
(controller as? SearchController)?.renderIsReplacingManga(isReplacingManga, newManga)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getEnabledSources(): List<CatalogueSource> {
|
||||
@ -55,7 +59,7 @@ class SearchPresenter(
|
||||
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) {
|
||||
val source = sourceManager.get(manga.source) ?: return
|
||||
|
||||
replacingMangaRelay.call(true)
|
||||
replacingMangaRelay.call(Pair(true, null))
|
||||
|
||||
presenterScope.launchIO {
|
||||
try {
|
||||
@ -67,7 +71,7 @@ class SearchPresenter(
|
||||
withUIContext { view?.applicationContext?.toast(e.message) }
|
||||
}
|
||||
|
||||
presenterScope.launchUI { replacingMangaRelay.call(false) }
|
||||
presenterScope.launchUI { replacingMangaRelay.call(Pair(false, manga)) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,9 +153,9 @@ class SearchPresenter(
|
||||
|
||||
// Update reading preferences
|
||||
manga.chapter_flags = prevManga.chapter_flags
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
manga.viewer = prevManga.viewer
|
||||
db.updateMangaViewer(manga).executeAsBlocking()
|
||||
db.updateChapterFlags(manga).executeAsBlocking()
|
||||
manga.viewer_flags = prevManga.viewer_flags
|
||||
db.updateViewerFlags(manga).executeAsBlocking()
|
||||
|
||||
// Update date added
|
||||
if (replace) {
|
||||
|
@ -52,7 +52,7 @@ class MigrationSourcesController :
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.source_migration, menu)
|
||||
inflater.inflate(R.menu.browse_migrate, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -254,7 +254,7 @@ class SourceController :
|
||||
createOptionsMenu(
|
||||
menu,
|
||||
inflater,
|
||||
R.menu.source_main,
|
||||
R.menu.browse_sources,
|
||||
R.id.action_search,
|
||||
R.string.action_global_search_hint,
|
||||
false // GlobalSearch handles the searching here
|
||||
|
@ -129,9 +129,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Prepare filter sheet
|
||||
initFilterSheet()
|
||||
|
||||
// Initialize adapter, scroll listener and recycler views
|
||||
adapter = FlexibleAdapter(null, this)
|
||||
setupRecycler(view)
|
||||
@ -173,11 +170,12 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
override fun configureFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFab = fab
|
||||
|
||||
// Controlled by initFilterSheet()
|
||||
fab.isVisible = false
|
||||
|
||||
fab.setText(R.string.action_filter)
|
||||
fab.setIconResource(R.drawable.ic_filter_list_24dp)
|
||||
|
||||
// Controlled by initFilterSheet()
|
||||
fab.isVisible = false
|
||||
initFilterSheet()
|
||||
}
|
||||
|
||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||
|
@ -9,6 +9,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
@ -30,6 +33,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
@ -102,6 +106,8 @@ open class BrowseSourcePresenter(
|
||||
*/
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
||||
|
||||
init {
|
||||
query = searchQuery ?: ""
|
||||
}
|
||||
@ -260,11 +266,36 @@ open class BrowseSourcePresenter(
|
||||
manga.removeCovers(coverCache)
|
||||
} else {
|
||||
ChapterSettingsHelper.applySettingDefaults(manga)
|
||||
|
||||
if (prefs.autoAddTrack()) {
|
||||
autoAddTrack(manga)
|
||||
}
|
||||
}
|
||||
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
private fun autoAddTrack(manga: Manga) {
|
||||
loggedServices
|
||||
.filterIsInstance<UnattendedTrackService>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
launchIO {
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id!!
|
||||
(service as TrackService).bind(track)
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
|
||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service as TrackService)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Could not match manga: ${manga.title} with service $service")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter states for the current source.
|
||||
*
|
||||
|
@ -1,11 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import coil.clear
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
|
||||
@ -42,14 +45,18 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
|
||||
// For rounded corners
|
||||
binding.card.clipToOutline = true
|
||||
|
||||
GlideApp.with(view.context).clear(binding.thumbnail)
|
||||
binding.thumbnail.clear()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(view.context)
|
||||
.load(manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(binding.thumbnail, binding.progress))
|
||||
val crossfadeDuration = view.context.imageLoader.defaults.transition.let {
|
||||
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||
}
|
||||
val request = ImageRequest.Builder(view.context)
|
||||
.data(manga)
|
||||
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
||||
.build()
|
||||
itemView.context.imageLoader.enqueue(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@ -14,28 +15,25 @@ import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||
|
||||
class SourceFilterSheet(
|
||||
activity: Activity,
|
||||
onFilterClicked: () -> Unit,
|
||||
onResetClicked: () -> Unit
|
||||
private val onFilterClicked: () -> Unit,
|
||||
private val onResetClicked: () -> Unit
|
||||
) : BaseBottomSheetDialog(activity) {
|
||||
|
||||
private var filterNavView: FilterNavigationView = FilterNavigationView(activity)
|
||||
private val sheetBehavior: BottomSheetBehavior<*>
|
||||
|
||||
init {
|
||||
override fun createView(inflater: LayoutInflater): View {
|
||||
filterNavView.onFilterClicked = {
|
||||
onFilterClicked()
|
||||
this.dismiss()
|
||||
}
|
||||
filterNavView.onResetClicked = onResetClicked
|
||||
|
||||
setContentView(filterNavView)
|
||||
|
||||
sheetBehavior = BottomSheetBehavior.from(filterNavView.parent as ViewGroup)
|
||||
return filterNavView
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
fun setFilters(items: List<IFlexible<*>>) {
|
||||
|
@ -1,11 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import coil.clear
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
|
||||
@ -42,14 +45,18 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl
|
||||
// For rounded corners
|
||||
binding.card.clipToOutline = true
|
||||
|
||||
GlideApp.with(view.context).clear(binding.thumbnail)
|
||||
binding.thumbnail.clear()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(view.context)
|
||||
.load(manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(binding.thumbnail, binding.progress))
|
||||
val crossfadeDuration = view.context.imageLoader.defaults.transition.let {
|
||||
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||
}
|
||||
val request = ImageRequest.Builder(view.context)
|
||||
.data(manga)
|
||||
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
|
||||
.build()
|
||||
itemView.context.imageLoader.enqueue(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.request.CachePolicy
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
@ -46,18 +45,14 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
||||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
GlideApp.with(view.context).clear(binding.thumbnail)
|
||||
|
||||
binding.thumbnail.clear()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius)
|
||||
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
|
||||
GlideApp.with(view.context)
|
||||
.load(manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.apply(requestOptions)
|
||||
.dontAnimate()
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(binding.thumbnail)
|
||||
val radius = view.context.resources.getDimension(R.dimen.card_radius)
|
||||
binding.thumbnail.loadAny(manga) {
|
||||
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
transformations(RoundedCornersTransformation(radius))
|
||||
diskCachePolicy(CachePolicy.DISABLED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||
import eu.kanade.tachiyomi.widget.listener.IgnoreFirstSpinnerListener
|
||||
|
||||
open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {
|
||||
|
||||
|
@ -1,11 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import coil.clear
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
|
||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||
|
||||
@ -42,15 +45,18 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
|
||||
}
|
||||
|
||||
fun setImage(manga: Manga) {
|
||||
GlideApp.with(itemView.context).clear(binding.cover)
|
||||
binding.cover.clear()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(binding.cover, binding.progress))
|
||||
val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
|
||||
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||
}
|
||||
val request = ImageRequest.Builder(itemView.context)
|
||||
.data(manga)
|
||||
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(StateImageViewTarget(binding.cover, binding.progress, crossfadeDuration))
|
||||
.build()
|
||||
itemView.context.imageLoader.enqueue(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,6 +356,15 @@ class DownloadController :
|
||||
val downloads = adapter.currentItems.mapNotNull { it?.download }
|
||||
presenter.reorder(downloads)
|
||||
}
|
||||
R.id.cancel_series -> {
|
||||
val download = adapter?.getItem(position)?.download ?: return
|
||||
val allDownloadsForSeries = adapter?.currentItems
|
||||
?.filter { download.manga.id == it.download.manga.id }
|
||||
?.map(DownloadItem::download)
|
||||
if (!allDownloadsForSeries.isNullOrEmpty()) {
|
||||
presenter.cancelDownloads(allDownloadsForSeries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,15 +81,14 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
|
||||
|
||||
private fun showPopupMenu(view: View) {
|
||||
view.popupMenu(
|
||||
R.menu.download_single,
|
||||
{
|
||||
menuRes = R.menu.download_single,
|
||||
initMenu = {
|
||||
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
|
||||
findItem(R.id.move_to_bottom).isVisible =
|
||||
bindingAdapterPosition != adapter.itemCount - 1
|
||||
},
|
||||
{
|
||||
onMenuItemClick = {
|
||||
adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this)
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -65,4 +65,8 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
||||
fun cancelDownload(download: Download) {
|
||||
downloadManager.deletePendingDownload(download)
|
||||
}
|
||||
|
||||
fun cancelDownloads(downloads: List<Download>) {
|
||||
downloadManager.deletePendingDownloads(*downloads.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,10 @@ package eu.kanade.tachiyomi.ui.library
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
|
||||
@ -51,18 +50,13 @@ class LibraryComfortableGridHolder(
|
||||
text = item.downloadCount.toString()
|
||||
}
|
||||
// set local visibility if its local manga
|
||||
binding.localText.isVisible = item.manga.isLocal()
|
||||
binding.localText.isVisible = item.isLocal
|
||||
|
||||
// For rounded corners
|
||||
binding.card.clipToOutline = true
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(view.context).clear(binding.thumbnail)
|
||||
GlideApp.with(view.context)
|
||||
.load(item.manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.dontAnimate()
|
||||
.into(binding.thumbnail)
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,9 @@ package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
|
||||
@ -49,18 +48,13 @@ open class LibraryCompactGridHolder(
|
||||
text = item.downloadCount.toString()
|
||||
}
|
||||
// set local visibility if its local manga
|
||||
binding.localText.isVisible = item.manga.isLocal()
|
||||
binding.localText.isVisible = item.isLocal
|
||||
|
||||
// For rounded corners
|
||||
binding.card.clipToOutline = true
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(view.context).clear(binding.thumbnail)
|
||||
GlideApp.with(view.context)
|
||||
.load(item.manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.dontAnimate()
|
||||
.into(binding.thumbnail)
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
}
|
||||
}
|
||||
|
@ -177,6 +177,7 @@ class LibraryController(
|
||||
adapter = LibraryAdapter(this)
|
||||
binding.libraryPager.adapter = adapter
|
||||
binding.libraryPager.pageSelections()
|
||||
.drop(1)
|
||||
.onEach {
|
||||
preferences.lastUsedCategory().set(it)
|
||||
activeCategory = it
|
||||
|
@ -28,6 +28,7 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
|
||||
|
||||
var downloadCount = -1
|
||||
var unreadCount = -1
|
||||
var isLocal = false
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return when (libraryDisplayMode.get()) {
|
||||
|
@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
|
||||
@ -53,7 +50,7 @@ class LibraryListHolder(
|
||||
text = "${item.downloadCount}"
|
||||
}
|
||||
// show local text badge if local manga
|
||||
binding.localText.isVisible = item.manga.isLocal()
|
||||
binding.localText.isVisible = item.isLocal
|
||||
|
||||
// Create thumbnail onclick to simulate long click
|
||||
binding.thumbnail.setOnClickListener {
|
||||
@ -62,15 +59,10 @@ class LibraryListHolder(
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
GlideApp.with(itemView.context).clear(binding.thumbnail)
|
||||
|
||||
val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius)
|
||||
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
|
||||
GlideApp.with(itemView.context)
|
||||
.load(item.manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.apply(requestOptions)
|
||||
.dontAnimate()
|
||||
.into(binding.thumbnail)
|
||||
val radius = view.context.resources.getDimension(R.dimen.card_radius)
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga) {
|
||||
transformations(RoundedCornersTransformation(radius))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -195,6 +195,7 @@ class LibraryPresenter(
|
||||
private fun setBadges(map: LibraryMap) {
|
||||
val showDownloadBadges = preferences.downloadBadge().get()
|
||||
val showUnreadBadges = preferences.unreadBadge().get()
|
||||
val showLocalBadges = preferences.localBadge().get()
|
||||
|
||||
for ((_, itemList) in map) {
|
||||
for (item in itemList) {
|
||||
@ -211,6 +212,13 @@ class LibraryPresenter(
|
||||
// Unset unread count if not enabled
|
||||
-1
|
||||
}
|
||||
|
||||
item.isLocal = if (showLocalBadges) {
|
||||
item.manga.isLocal()
|
||||
} else {
|
||||
// Hide / Unset local badge if not enabled
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,14 +276,16 @@ class LibrarySettingsSheet(
|
||||
inner class BadgeGroup : Group {
|
||||
private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
|
||||
private val unreadBadge = Item.CheckboxGroup(R.string.action_display_unread_badge, this)
|
||||
private val localBadge = Item.CheckboxGroup(R.string.action_display_local_badge, this)
|
||||
|
||||
override val header = Item.Header(R.string.badges_header)
|
||||
override val items = listOf(downloadBadge, unreadBadge)
|
||||
override val items = listOf(downloadBadge, unreadBadge, localBadge)
|
||||
override val footer = null
|
||||
|
||||
override fun initModels() {
|
||||
downloadBadge.checked = preferences.downloadBadge().get()
|
||||
unreadBadge.checked = preferences.unreadBadge().get()
|
||||
localBadge.checked = preferences.localBadge().get()
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
@ -292,6 +294,7 @@ class LibrarySettingsSheet(
|
||||
when (item) {
|
||||
downloadBadge -> preferences.downloadBadge().set((item.checked))
|
||||
unreadBadge -> preferences.unreadBadge().set((item.checked))
|
||||
localBadge -> preferences.localBadge().set((item.checked))
|
||||
}
|
||||
adapter.notifyItemChanged(item)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user