Compare commits

..

1 Commits

Author SHA1 Message Date
Jobobby04 cbf82a9d6a Hide dedupe by priority 2021-04-11 21:53:50 -04:00
546 changed files with 10282 additions and 12907 deletions
+1
View File
@@ -1 +1,2 @@
github: inorichi
ko_fi: inorichi ko_fi: inorichi
+2 -8
View File
@@ -2,15 +2,9 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated to the latest version of the app (stable is v1.6.0)
- To the latest version of the app (stable is v1.7.0) - I have updated all extensions
- 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 - 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
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+2 -8
View File
@@ -9,15 +9,9 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated: - I have updated to the latest version of the app (stable is v1.6.0)
- To the latest version of the app (stable is v1.7.0) - I have updated all extensions
- 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 - 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
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
+2 -7
View File
@@ -9,14 +9,9 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated: - I have updated to the latest version of the app (stable is v1.6.0)
- To the latest version of the app (stable is v1.7.0) - I have updated all extensions
- All extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Autoclose issues - name: Autoclose issues
uses: arkon/issue-closer-action@v3.4 uses: arkon/issue-closer-action@v3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: | rules: |
-14
View File
@@ -1,14 +0,0 @@
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 }}
+2 -2
View File
@@ -4,14 +4,14 @@
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko. Tachiyomi is a free and open source manga reader for Android 5.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
![screenshots of app](./.github/readme-images/screens.png) ![screenshots of app](./.github/readme-images/screens.png)
## Features ## Features
Features of Tachiyomi(original) include: Features of Tachiyomi(original) include:
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions) * Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Local reading of downloaded manga * Local reading of downloaded manga
* A configurable reader with multiple viewers, reading directions and other settings. * A configurable reader with multiple viewers, reading directions and other settings.
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support * [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
+40 -34
View File
@@ -8,11 +8,11 @@ plugins {
id("com.android.application") id("com.android.application")
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
kotlin("android") kotlin("android")
kotlin("kapt")
kotlin("plugin.parcelize") kotlin("plugin.parcelize")
kotlin("plugin.serialization") kotlin("plugin.serialization")
id("com.github.zellius.shortcut-helper") id("com.github.zellius.shortcut-helper")
// Realm (EH) // Realm (EH)
kotlin("kapt")
id("realm-android") id("realm-android")
} }
@@ -34,8 +34,8 @@ android {
minSdkVersion(AndroidConfig.minSdk) minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk) targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 19 versionCode = 14
versionName = "1.7.0" versionName = "1.6.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -62,12 +62,14 @@ android {
applicationIdSuffix = ".rt" applicationIdSuffix = ".rt"
//isMinifyEnabled = true //isMinifyEnabled = true
//isShrinkResources = true //isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) isZipAlignEnabled = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
} }
named("release") { named("release") {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) isZipAlignEnabled = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
} }
} }
@@ -122,34 +124,34 @@ android {
dependencies { dependencies {
// Source models and interfaces from Tachiyomi 1.x // Source models and interfaces from Tachiyomi 1.x
implementation("org.tachiyomi:source-api:1.1") implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries // AndroidX libraries
implementation("androidx.annotation:annotation:1.3.0-alpha01") implementation("androidx.annotation:annotation:1.3.0-alpha01")
implementation("androidx.appcompat:appcompat:1.4.0-alpha01") implementation("androidx.appcompat:appcompat:1.3.0-rc01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03") implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation("androidx.browser:browser:1.3.0") implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02") implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0") implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.6.0-beta01") implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0") implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val lifecycleVersion = "2.4.0-alpha01" val lifecycleVersion = "2.3.0"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
// Job scheduling // Job scheduling
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03") implementation("androidx.work:work-runtime-ktx:2.5.0")
// UI library // UI library
implementation("com.google.android.material:material:1.4.0-beta01") implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:19.0.0") "standardImplementation"("com.google.firebase:firebase-core:18.0.3")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxandroid:1.2.1") implementation("io.reactivex:rxandroid:1.2.1")
@@ -158,7 +160,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0") implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client // Network client
val okhttpVersion = "4.9.1" val okhttpVersion = "5.0.0-alpha.2"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@@ -168,7 +170,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1") implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON // JSON
val kotlinSerializationVersion = "1.1.0" val kotlinSerializationVersion = "1.0.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6") implementation("com.google.code.gson:gson:2.8.6")
@@ -179,7 +181,7 @@ dependencies {
// Disk // Disk
implementation("com.jakewharton:disklrucache:2.0.2") implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.tachiyomiorg:unifile:17bec43") implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.junrar:junrar:7.4.0") implementation("com.github.junrar:junrar:7.4.0")
// HTML parser // HTML parser
@@ -189,10 +191,10 @@ dependencies {
implementation("androidx.sqlite:sqlite-ktx:2.1.0") implementation("androidx.sqlite:sqlite-ktx:2.1.0")
implementation("com.github.inorichi.storio:storio-common:8be19de@aar") implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar") implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
implementation("com.github.requery:sqlite-android:3.35.5") implementation("io.requery:sqlite-android:3.33.0")
// Preferences // Preferences
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0") implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
// Model View Presenter // Model View Presenter
val nucleusVersion = "3.0.0" val nucleusVersion = "3.0.0"
@@ -203,14 +205,12 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library // Image library
val coilVersion = "1.2.0" val glideVersion = "4.12.0"
implementation("io.coil-kt:coil:$coilVersion") implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("io.coil-kt:coil-gif:$coilVersion") implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") { implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
exclude(module = "image-decoder")
}
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
// Logging // Logging
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")
@@ -222,13 +222,14 @@ dependencies {
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1") implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// UI // UI
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.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:5.1.0")
implementation("eu.davidea:flexible-adapter-ui:1.0.0") implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("dev.chrisbanes.insetter:insetter:0.6.0") implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices // 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1" val materialDialogsVersion = "3.1.1"
@@ -244,7 +245,7 @@ dependencies {
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1") implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
// FlowBinding // FlowBinding
val flowbindingVersion = "1.0.0" val flowbindingVersion = "0.12.0"
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion") implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion") implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion") implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
@@ -266,12 +267,12 @@ dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN)) implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
val coroutinesVersion = "1.5.0" val coroutinesVersion = "1.4.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7") // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
// SY --> // SY -->
// [EXH] Android 7 SSL Workaround // [EXH] Android 7 SSL Workaround
@@ -284,8 +285,8 @@ dependencies {
implementation ("info.debatty:java-string-similarity:2.0.0") implementation ("info.debatty:java-string-similarity:2.0.0")
// Firebase (EH) // Firebase (EH)
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0") implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
implementation("com.google.firebase:firebase-crashlytics-ktx:18.0.0") implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
// Better logging (EH) // Better logging (EH)
implementation("com.elvishew:xlog:1.9.0") implementation("com.elvishew:xlog:1.9.0")
@@ -298,7 +299,13 @@ dependencies {
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion") testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
// RatingBar (SY) // RatingBar (SY)
implementation("me.zhanghai.android.materialratingbar:library:1.4.0") implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
// JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.12.0")
implementation("com.mikepenz:fastadapter:5.4.0")
// SY <--
} }
tasks { tasks {
@@ -311,8 +318,7 @@ tasks {
"-Xuse-experimental=kotlinx.coroutines.FlowPreview", "-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi", "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi", "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
) )
} }
-34
View File
@@ -1,34 +0,0 @@
-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>(...);
}
+42 -15
View File
@@ -58,7 +58,6 @@
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
# Filter serializer
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; } -keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** { -keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
*** Companion; *** Companion;
@@ -67,22 +66,37 @@
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
# Keep extension's common dependencies # Madokami extension username and password crash fix
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; } -keepclassmembers class androidx.preference.EditTextPreference {
-keep,allowoptimization class kotlin.** { public protected *; } *** mOnBindEditTextListener;
-keep,allowoptimization class okhttp3.** { public protected *; } *** mText;
-keep,allowoptimization class rx.** { public protected *; } public *;
-keep,allowoptimization class org.jsoup.** { public protected *; } }
-keep,allowoptimization class com.google.gson.** { public protected *; }
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; } # Hitomi extension crash fix
-keep,allowoptimization class com.squareup.duktape.** { public protected *; } -keepclassmembers class rx.Single {
-keep,allowoptimization class androidx.preference.** { *; } *** onSubscribe;
-keep,allowoptimization class okio.** { *; } final *;
-keep,allowoptimization class kotlinx.serialization.** { *; } protected *;
public *;
}
# RxJava 1.1.0 # RxJava 1.1.0
-dontwarn sun.misc.** -dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
-dontnote rx.internal.util.PlatformDependent -dontnote rx.internal.util.PlatformDependent
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration # === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
@@ -119,9 +133,8 @@
# Application classes that will be serialized/deserialized over Gson # Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; } -keep class com.google.gson.examples.android.model.** { <fields>; }
# Prevent proguard from stripping interface information from TypeAdapterFactory, TypeAdapter, # Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer -keep class * implements com.google.gson.JsonDeserializer
@@ -142,6 +155,20 @@
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1" ## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
-keep class uy.kohesive.injekt.** { *; } -keep class uy.kohesive.injekt.** { *; }
# === Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
# === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt
-dontwarn jp.co.cyberagent.android.gpuimage.**
# === Conductor # === Conductor
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files # This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler { -keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
+1 -1
View File
@@ -17,7 +17,7 @@
android:shortcutDisabledMessage="@string/app_not_available" android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated" android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates" android:shortcutLongLabel="@string/label_recent_updates"
android:shortcutShortLabel="@string/label_recent_updates"> android:shortcutShortLabel="@string/short_recent_updates">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
+9 -5
View File
@@ -33,7 +33,7 @@
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Base" android:theme="@style/Theme.Tachiyomi.Light"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
@@ -84,8 +84,8 @@
android:resource="@xml/s_pen_actions"/> android:resource="@xml/s_pen_actions"/>
</activity> </activity>
<activity <activity
android:name=".ui.security.UnlockActivity" android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Base" /> android:theme="@style/Theme.Splash" />
<activity <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" /> android:configChanges="uiMode|orientation|screenSize" />
@@ -189,6 +189,10 @@
android:exported="false" /> android:exported="false" />
<!-- EH --> <!-- EH -->
<service
android:name="exh.md.similar.SimilarUpdateService"
android:exported="false" />
<service <service
android:name="exh.eh.EHentaiUpdateWorker" android:name="exh.eh.EHentaiUpdateWorker"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
@@ -196,7 +200,7 @@
<activity <activity
android:name="exh.ui.intercept.InterceptActivity" android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Base"> android:theme="@style/Theme.EHActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -370,7 +374,7 @@
</activity> </activity>
<activity <activity
android:name="exh.ui.captcha.BrowserActionActivity" android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.Base" /> android:theme="@style/Theme.EHActivity" />
</application> </application>
</manifest> </manifest>
+1 -100
View File
@@ -1,29 +1,16 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.webkit.WebView
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import com.elvishew.xlog.LogConfiguration import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel import com.elvishew.xlog.LogLevel
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
@@ -39,14 +26,10 @@ import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.DebugOverlay
import com.ms_square.debugoverlay.modules.FpsModule import com.ms_square.debugoverlay.modules.FpsModule
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.notification
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.log.CrashlyticsPrinter import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay import exh.log.EHDebugModeOverlay
@@ -57,12 +40,9 @@ import exh.log.xLogD
import exh.log.xLogE import exh.log.xLogE
import exh.syDebugVersion import exh.syDebugVersion
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
@@ -73,12 +53,10 @@ import javax.net.ssl.SSLContext
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.days import kotlin.time.days
open class App : Application(), LifecycleObserver, ImageLoaderFactory { open class App : Application(), LifecycleObserver {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) // if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
@@ -93,12 +71,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
Security.insertProviderAt(Conscrypt.newProvider(), 1) 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)) Injekt.importModule(AppModule(this))
setupNotificationChannels() setupNotificationChannels()
@@ -110,34 +82,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
LocaleHelper.updateConfiguration(this, resources.configuration) LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this) 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) { override fun attachBaseContext(base: Context) {
@@ -150,23 +94,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
LocaleHelper.updateConfiguration(this, newConfig, true) 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()
}
private fun workaroundAndroid7BrokenSSL() { private fun workaroundAndroid7BrokenSSL() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N || if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
@@ -288,30 +215,4 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
xLogE("Failed to initialize debug overlay, app in background?", e) xLogE("Failed to initialize debug overlay, app in background?", e)
} }
} }
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,6 +2,7 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.os.Handler import android.os.Handler
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -43,6 +44,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { TrackManager(app) } addSingletonFactory { TrackManager(app) }
addSingletonFactory { Gson() }
addSingletonFactory { Json { ignoreUnknownKeys = true } } addSingletonFactory { Json { ignoreUnknownKeys = true } }
// SY --> // SY -->
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.os.Build
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
@@ -12,7 +11,6 @@ import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibrarySort 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.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -143,50 +141,6 @@ 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) {
// 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 return true
} }
@@ -8,6 +8,7 @@ object BackupConst {
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI" const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS" const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE" 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_LEGACY = 0
const val BACKUP_TYPE_FULL = 1 const val BACKUP_TYPE_FULL = 1
@@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
@@ -33,9 +34,7 @@ class BackupCreateService : Service() {
// SY --> // SY -->
internal const val BACKUP_CUSTOM_INFO = 0x10 internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10 internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_READ_MANGA = 0x20 internal const val BACKUP_ALL = 0x1F
internal const val BACKUP_READ_MANGA_MASK = 0x20
internal const val BACKUP_ALL = 0x3F
// SY <-- // SY <--
/** /**
@@ -54,11 +53,12 @@ class BackupCreateService : Service() {
* @param uri path of Uri * @param uri path of Uri
* @param flags determines what to backup * @param flags determines what to backup
*/ */
fun start(context: Context, uri: Uri, flags: Int) { fun start(context: Context, uri: Uri, flags: Int, type: Int) {
if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, BackupCreateService::class.java).apply { val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri) putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_FLAGS, flags) putExtra(BackupConst.EXTRA_FLAGS, flags)
putExtra(BackupConst.EXTRA_TYPE, type)
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
@@ -106,11 +106,17 @@ class BackupCreateService : Service() {
if (intent == null) return START_NOT_STICKY if (intent == null) return START_NOT_STICKY
try { 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 backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri() 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 unifile = UniFile.fromUri(this, backupFileUri) val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile) notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
} catch (e: Exception) { } catch (e: Exception) {
notifier.showBackupError(e.message) notifier.showBackupError(e.message)
} }
@@ -8,6 +8,7 @@ import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -22,6 +23,9 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
val flags = BackupCreateService.BACKUP_ALL val flags = BackupCreateService.BACKUP_ALL
return try { return try {
FullBackupManager(context).createBackup(uri, flags, true) FullBackupManager(context).createBackup(uri, flags, true)
if (preferences.createLegacyBackup().get()) {
LegacyBackupManager(context).createBackup(uri, flags, true)
}
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
Result.failure() Result.failure()
@@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
} }
} }
fun showBackupComplete(unifile: UniFile) { fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS) context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
with(completeNotificationBuilder) { with(completeNotificationBuilder) {
@@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_share_24dp, R.drawable.ic_share_24dp,
context.getString(R.string.action_share), context.getString(R.string.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE) NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
) )
show(Notifications.ID_BACKUP_COMPLETE) show(Notifications.ID_BACKUP_COMPLETE)
@@ -12,8 +12,6 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUST
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY 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_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK 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.BackupCreateService.Companion.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.full.models.Backup import eu.kanade.tachiyomi.data.backup.full.models.Backup
@@ -66,11 +64,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
var backup: Backup? = null var backup: Backup? = null
databaseHelper.inTransaction { databaseHelper.inTransaction {
val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) { val databaseManga = getFavoriteManga() /* SY --> */ + getReadManga() + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
getReadManga()
} else {
emptyList()
} + getMergedManga() /* SY <-- */
backup = Backup( backup = Backup(
backupManga(databaseManga, flags), backupManga(databaseManga, flags),
@@ -26,7 +26,7 @@ data class BackupManga(
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x // @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(12) val lastInit: Long = 0, 1.x value, not used in 0.x
@ProtoNumber(13) var dateAdded: Long = 0, @ProtoNumber(13) var dateAdded: Long = 0,
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags @ProtoNumber(14) var viewer: Int = 0,
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x // @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(), @ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
@ProtoNumber(17) var categories: List<Int> = emptyList(), @ProtoNumber(17) var categories: List<Int> = emptyList(),
@@ -35,8 +35,6 @@ data class BackupManga(
@ProtoNumber(100) var favorite: Boolean = true, @ProtoNumber(100) var favorite: Boolean = true,
@ProtoNumber(101) var chapterFlags: Int = 0, @ProtoNumber(101) var chapterFlags: Int = 0,
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(), @ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int? = null,
// SY specific values // SY specific values
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(), @ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null, @ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
@@ -47,10 +45,7 @@ data class BackupManga(
@ProtoNumber(801) var customArtist: String? = null, @ProtoNumber(801) var customArtist: String? = null,
@ProtoNumber(802) var customAuthor: String? = null, @ProtoNumber(802) var customAuthor: String? = null,
@ProtoNumber(803) var customDescription: String? = null, @ProtoNumber(803) var customDescription: String? = null,
@ProtoNumber(803) var customGenre: List<String>? = null, @ProtoNumber(803) var customGenre: List<String>? = null
// Neko specific values
@ProtoNumber(901) var filtered_scanlators: String? = null,
) { ) {
fun getMangaImpl(): MangaImpl { fun getMangaImpl(): MangaImpl {
return MangaImpl().apply { return MangaImpl().apply {
@@ -65,9 +60,8 @@ data class BackupManga(
favorite = this@BackupManga.favorite favorite = this@BackupManga.favorite
source = this@BackupManga.source source = this@BackupManga.source
date_added = this@BackupManga.dateAdded date_added = this@BackupManga.dateAdded
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer viewer = this@BackupManga.viewer
chapter_flags = this@BackupManga.chapterFlags chapter_flags = this@BackupManga.chapterFlags
filtered_scanlators = this@BackupManga.filtered_scanlators
} }
} }
@@ -122,10 +116,8 @@ data class BackupManga(
favorite = manga.favorite, favorite = manga.favorite,
source = manga.source, source = manga.source,
dateAdded = manga.date_added, dateAdded = manga.date_added,
viewer = manga.readingModeType, viewer = manga.viewer,
viewer_flags = manga.viewer_flags, chapterFlags = manga.chapter_flags
chapterFlags = manga.chapter_flags,
filtered_scanlators = manga.filtered_scanlators
// SY --> // SY -->
).also { backupManga -> ).also { backupManga ->
customMangaManager?.getManga(manga)?.let { customMangaManager?.getManga(manga)?.let {
@@ -5,13 +5,33 @@ import android.net.Uri
import com.github.salomonbrys.kotson.fromJson import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.registerTypeAdapter import com.github.salomonbrys.kotson.registerTypeAdapter
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
import com.github.salomonbrys.kotson.set
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.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.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.MERGEDMANGAREFERENCES
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.SAVEDSEARCHES
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.models.DHistory
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
@@ -29,6 +49,8 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
@@ -40,6 +62,8 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.RuntimeException import java.lang.RuntimeException
import kotlin.math.max import kotlin.math.max
@@ -65,8 +89,180 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
* @param uri path of Uri * @param uri path of Uri
* @param isJob backup called from job * @param isJob backup called from job
*/ */
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) = override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
throw IllegalStateException("Legacy backup creation is not supported") // 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()
// Merged Manga References
val mergedMangaReferenceEntries = JsonArray()
// Add value's to root
root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries
root[EXTENSIONS] = extensionEntries
// SY -->
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
// SY <--
databaseHelper.inTransaction {
val mangas = getFavoriteManga()/* SY --> */.filterNot { it.source == MERGED_SOURCE_ID } + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
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)
// SY -->
root[SAVEDSEARCHES] =
Injekt.get<PreferencesHelper>().savedSearches().get().joinToString(separator = "***")
backupMergedMangaReferences(mergedMangaReferenceEntries)
// SY <--
}
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)
}
}
// SY -->
private fun backupMergedMangaReferences(root: JsonArray) {
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
}
// SY <--
/**
* 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
}
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) { fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
manga.id = dbManga.id manga.id = dbManga.id
@@ -18,7 +18,7 @@ object MangaTypeAdapter {
value(it.originalTitle) value(it.originalTitle)
// SY <-- // SY <--
value(it.source) value(it.source)
value(it.viewer_flags) value(it.viewer)
value(it.chapter_flags) value(it.chapter_flags)
endArray() endArray()
} }
@@ -29,7 +29,7 @@ object MangaTypeAdapter {
manga.url = nextString() manga.url = nextString()
manga.title = nextString() manga.title = nextString()
manga.source = nextLong() manga.source = nextLong()
manga.viewer_flags = nextInt() manga.viewer = nextInt()
manga.chapter_flags = nextInt() manga.chapter_flags = nextInt()
endArray() endArray()
manga manga
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -13,12 +15,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Response import okhttp3.Response
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -48,12 +48,14 @@ class ChapterCache(private val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
/** Google Json class used for parsing JSON files. */ /** Google Json class used for parsing JSON files. */
private val json: Json by injectLazy() private val gson: Gson by injectLazy()
// --> EH // --> EH
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
// <-- EH
/** Cache class used for cache management. */ /** Cache class used for cache management. */
// --> EH
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong()) private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
init { init {
@@ -71,7 +73,7 @@ class ChapterCache(private val context: Context) {
/** /**
* Returns directory of cache. * Returns directory of cache.
*/ */
private val cacheDir: File val cacheDir: File
get() = diskCache.directory get() = diskCache.directory
/** /**
@@ -98,19 +100,43 @@ class ChapterCache(private val context: Context) {
} }
// <-- EH // <-- EH
/**
* Remove file from cache.
*
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal.")) {
return false
}
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
diskCache.remove(key)
} catch (e: Exception) {
false
}
}
/** /**
* Get page list from cache. * Get page list from cache.
* *
* @param chapter the chapter. * @param chapter the chapter.
* @return the list of pages. * @return an observable of the list of pages.
*/ */
fun getPageListFromCache(chapter: Chapter): List<Page> { fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
// Get the key for the chapter. return Observable.fromCallable {
val key = DiskUtil.hashKeyForDisk(getKey(chapter)) // Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null // Convert JSON string to list of objects. Throws an exception if snapshot is null
return diskCache.get(key).use { diskCache.get(key).use {
json.decodeFromString(it.getString(0)) gson.fromJson<List<Page>>(it.getString(0))
}
} }
} }
@@ -122,7 +148,7 @@ class ChapterCache(private val context: Context) {
*/ */
fun putPageListToCache(chapter: Chapter, pages: List<Page>) { fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string. // Convert list of pages to json string.
val cachedValue = json.encodeToString(pages) val cachedValue = gson.toJson(pages)
// Initialize the editor (edits the values for an entry). // Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null var editor: DiskLruCache.Editor? = null
@@ -202,38 +228,6 @@ class ChapterCache(private val context: Context) {
} }
} }
fun clear(): Int {
var deletedFiles = 0
cacheDir.listFiles()?.forEach {
if (removeFileFromCache(it.name)) {
deletedFiles++
}
}
return deletedFiles
}
/**
* Remove file from cache.
*
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
private fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal.")) {
return false
}
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
diskCache.remove(key)
} catch (e: Exception) {
false
}
}
private fun getKey(chapter: Chapter): String { private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}" return "${chapter.manga_id}${chapter.url}"
} }
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.cache package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import coil.imageLoader
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import java.io.File import java.io.File
@@ -100,13 +99,6 @@ class CoverCache(private val context: Context) {
} }
} }
/**
* Clear coil's memory cache.
*/
fun clearMemoryCache() {
context.imageLoader.memoryCache.clear()
}
private fun getCacheDir(dir: String): File { private fun getCacheDir(dir: String): File {
return context.getExternalFilesDir(dir) return context.getExternalFilesDir(dir)
?: File(context.filesDir, dir).also { it.mkdirs() } ?: File(context.filesDir, dir).also { it.mkdirs() }
@@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import okio.buffer
import okio.source
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
class ByteBufferFetcher : Fetcher<ByteBuffer> {
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
return SourceResult(
source = ByteArrayInputStream(data.array()).source().buffer(),
mimeType = null,
dataSource = DataSource.MEMORY
)
}
override fun key(data: ByteBuffer): String? = null
}
@@ -1,172 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.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()
}
}
@@ -21,6 +21,9 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaQueries import eu.kanade.tachiyomi.data.database.queries.MangaQueries
import eu.kanade.tachiyomi.data.database.queries.TrackQueries import eu.kanade.tachiyomi.data.database.queries.TrackQueries
import exh.md.similar.sql.mappers.SimilarTypeMapping
import exh.md.similar.sql.models.MangaSimilar
import exh.md.similar.sql.queries.SimilarQueries
import exh.merged.sql.mappers.MergedMangaTypeMapping import exh.merged.sql.mappers.MergedMangaTypeMapping
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.merged.sql.queries.MergedQueries import exh.merged.sql.queries.MergedQueries
@@ -39,7 +42,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) : open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ { MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
@@ -59,6 +62,7 @@ open class DatabaseHelper(context: Context) :
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping()) .addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping()) .addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
.addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
// SY <-- // SY <--
.build() .build()
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable import eu.kanade.tachiyomi.data.database.tables.TrackTable
import exh.md.similar.sql.tables.SimilarTable
import exh.merged.sql.tables.MergedTable import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable import exh.metadata.sql.tables.SearchTagTable
@@ -24,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = /* SY --> */ 7 /* SY <-- */ const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -39,6 +40,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTagTable.createTableQuery) execSQL(SearchTagTable.createTableQuery)
execSQL(SearchTitleTable.createTableQuery) execSQL(SearchTitleTable.createTableQuery)
execSQL(MergedTable.createTableQuery) execSQL(MergedTable.createTableQuery)
execSQL(SimilarTable.createTableQuery)
// SY <-- // SY <--
// DB indexes // DB indexes
@@ -55,6 +57,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTitleTable.createMangaIdIndexQuery) execSQL(SearchTitleTable.createMangaIdIndexQuery)
execSQL(SearchTitleTable.createTitleIndexQuery) execSQL(SearchTitleTable.createTitleIndexQuery)
execSQL(MergedTable.createIndexQuery) execSQL(MergedTable.createIndexQuery)
execSQL(SimilarTable.createMangaIdIndexQuery)
// SY <-- // SY <--
} }
@@ -71,15 +74,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(MergedTable.createTableQuery) db.execSQL(MergedTable.createTableQuery)
db.execSQL(MergedTable.createIndexQuery) db.execSQL(MergedTable.createIndexQuery)
} }
/*if (oldVersion < 5) { if (oldVersion < 5) {
db.execSQL(SimilarTable.createTableQuery) db.execSQL(SimilarTable.createTableQuery)
db.execSQL(SimilarTable.createMangaIdIndexQuery) db.execSQL(SimilarTable.createMangaIdIndexQuery)
}*/
if (oldVersion < 6) {
db.execSQL(MangaTable.addFilteredScanlators)
}
if (oldVersion < 7) {
db.execSQL("DROP TABLE IF EXISTS manga_related")
} }
} }
@@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
@@ -66,11 +65,10 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_FAVORITE to obj.favorite, COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update, COL_LAST_UPDATE to obj.last_update,
COL_INITIALIZED to obj.initialized, COL_INITIALIZED to obj.initialized,
COL_VIEWER to obj.viewer_flags, COL_VIEWER to obj.viewer,
COL_CHAPTER_FLAGS to obj.chapter_flags, COL_CHAPTER_FLAGS to obj.chapter_flags,
COL_COVER_LAST_MODIFIED to obj.cover_last_modified, COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
COL_DATE_ADDED to obj.date_added, COL_DATE_ADDED to obj.date_added
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
) )
} }
@@ -89,11 +87,10 @@ interface BaseMangaGetResolver {
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1 favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE)) last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1 initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER)) viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED)) cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED)) date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
} }
} }
@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SManga 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 import tachiyomi.source.model.MangaInfo
interface Manga : SManga { interface Manga : SManga {
@@ -17,20 +15,18 @@ interface Manga : SManga {
var date_added: Long var date_added: Long
var viewer_flags: Int var viewer: Int
var chapter_flags: Int var chapter_flags: Int
var cover_last_modified: Long var cover_last_modified: Long
var filtered_scanlators: String?
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setChapterFlags(order, CHAPTER_SORT_MASK) setFlags(order, SORT_MASK)
} }
fun sortDescending(): Boolean { fun sortDescending(): Boolean {
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC return chapter_flags and SORT_MASK == SORT_DESC
} }
fun getGenres(): List<String>? { fun getGenres(): List<String>? {
@@ -43,72 +39,60 @@ interface Manga : SManga {
} }
// SY <-- // SY <--
private fun setChapterFlags(flag: Int, mask: Int) { private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask) 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 // Used to display the chapter's title one way or another
var displayMode: Int var displayMode: Int
get() = chapter_flags and CHAPTER_DISPLAY_MASK get() = chapter_flags and DISPLAY_MASK
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK) set(mode) = setFlags(mode, DISPLAY_MASK)
var readFilter: Int var readFilter: Int
get() = chapter_flags and CHAPTER_READ_MASK get() = chapter_flags and READ_MASK
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK) set(filter) = setFlags(filter, READ_MASK)
var downloadedFilter: Int var downloadedFilter: Int
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK get() = chapter_flags and DOWNLOADED_MASK
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK) set(filter) = setFlags(filter, DOWNLOADED_MASK)
var bookmarkedFilter: Int var bookmarkedFilter: Int
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK get() = chapter_flags and BOOKMARKED_MASK
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK) set(filter) = setFlags(filter, BOOKMARKED_MASK)
var sorting: Int var sorting: Int
get() = chapter_flags and CHAPTER_SORTING_MASK get() = chapter_flags and SORTING_MASK
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK) set(sort) = setFlags(sort, 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 { companion object {
const val SORT_DESC = 0x00000000
const val SORT_ASC = 0x00000001
const val SORT_MASK = 0x00000001
// Generic filter that does not filter anything // Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000 const val SHOW_ALL = 0x00000000
const val CHAPTER_SORT_DESC = 0x00000000 const val SHOW_UNREAD = 0x00000002
const val CHAPTER_SORT_ASC = 0x00000001 const val SHOW_READ = 0x00000004
const val CHAPTER_SORT_MASK = 0x00000001 const val READ_MASK = 0x00000006
const val CHAPTER_SHOW_UNREAD = 0x00000002 const val SHOW_DOWNLOADED = 0x00000008
const val CHAPTER_SHOW_READ = 0x00000004 const val SHOW_NOT_DOWNLOADED = 0x00000010
const val CHAPTER_READ_MASK = 0x00000006 const val DOWNLOADED_MASK = 0x00000018
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008 const val SHOW_BOOKMARKED = 0x00000020
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010 const val SHOW_NOT_BOOKMARKED = 0x00000040
const val CHAPTER_DOWNLOADED_MASK = 0x00000018 const val BOOKMARKED_MASK = 0x00000060
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020 const val SORTING_SOURCE = 0x00000000
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040 const val SORTING_NUMBER = 0x00000100
const val CHAPTER_BOOKMARKED_MASK = 0x00000060 const val SORTING_UPLOAD_DATE = 0x00000200
const val SORTING_MASK = 0x00000300
const val CHAPTER_SORTING_SOURCE = 0x00000000 const val DISPLAY_NAME = 0x00000000
const val CHAPTER_SORTING_NUMBER = 0x00000100 const val DISPLAY_NUMBER = 0x00100000
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200 const val DISPLAY_MASK = 0x00100000
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 { fun create(source: Long): Manga = MangaImpl().apply {
this.source = source this.source = source
@@ -56,14 +56,12 @@ open class MangaImpl : Manga {
override var initialized: Boolean = false override var initialized: Boolean = false
override var viewer_flags: Int = 0 override var viewer: Int = 0
override var chapter_flags: Int = 0 override var chapter_flags: Int = 0
override var cover_last_modified: Long = 0 override var cover_last_modified: Long = 0
override var filtered_scanlators: String? = null
// SY --> // SY -->
lateinit var ogTitle: String lateinit var ogTitle: String
private set private set
@@ -9,13 +9,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver 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.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@@ -114,24 +114,14 @@ interface MangaQueries : DbProvider {
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare() fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateChapterFlags(manga: Manga) = db.put() fun updateFlags(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags)) .withPutResolver(MangaFlagsPutResolver())
.prepare() .prepare()
fun updateChapterFlags(manga: List<Manga>) = db.put() fun updateFlags(mangas: List<Manga>) = db.put()
.objects(manga) .objects(mangas)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true)) .withPutResolver(MangaFlagsPutResolver(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() .prepare()
fun updateLastUpdated(manga: Manga) = db.put() fun updateLastUpdated(manga: Manga) = db.put()
@@ -144,6 +134,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaFavoritePutResolver()) .withPutResolver(MangaFavoritePutResolver())
.prepare() .prepare()
fun updateMangaViewer(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaViewerPutResolver())
.prepare()
fun updateMangaTitle(manga: Manga) = db.put() fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaTitlePutResolver()) .withPutResolver(MangaTitlePutResolver())
@@ -154,13 +149,6 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaCoverLastModifiedPutResolver()) .withPutResolver(MangaCoverLastModifiedPutResolver())
.prepare() .prepare()
// SY -->
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFilteredScanlatorsPutResolver())
.prepare()
// SY <--
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
@@ -8,9 +8,8 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import kotlin.reflect.KProperty1
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() { class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga) val updateQuery = mapToUpdateQuery(manga)
@@ -38,6 +37,6 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
fun mapToContentValues(manga: Manga) = fun mapToContentValues(manga: Manga) =
contentValuesOf( contentValuesOf(
colName to fieldGetter.get(manga) MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
) )
} }
@@ -30,6 +30,6 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
MangaTable.COL_DATE_ADDED to manga.date_added, MangaTable.COL_DATE_ADDED to manga.date_added,
MangaTable.COL_TITLE to manga.title, MangaTable.COL_TITLE to manga.title,
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags, MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
MangaTable.COL_VIEWER to manga.viewer_flags MangaTable.COL_VIEWER to manga.viewer
) )
} }
@@ -9,8 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
// [EXH] class MangaViewerPutResolver : PutResolver<Manga>() {
class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga) val updateQuery = mapToUpdateQuery(manga)
@@ -22,11 +21,12 @@ class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FILTERED_SCANLATORS} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.filtered_scanlators) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = contentValuesOf( fun mapToContentValues(manga: Manga) =
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators contentValuesOf(
) MangaTable.COL_VIEWER to manga.viewer
)
} }
@@ -40,8 +40,6 @@ object MangaTable {
// SY ->> // SY ->>
const val COL_READ = "read" const val COL_READ = "read"
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
// SY <-- // SY <--
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
@@ -67,8 +65,7 @@ object MangaTable {
$COL_VIEWER INTEGER NOT NULL, $COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL, $COL_CHAPTER_FLAGS INTEGER NOT NULL,
$COL_COVER_LAST_MODIFIED LONG NOT NULL, $COL_COVER_LAST_MODIFIED LONG NOT NULL,
$COL_DATE_ADDED LONG NOT NULL, $COL_DATE_ADDED LONG NOT NULL
$COL_FILTERED_SCANLATORS TEXT
)""" )"""
val createUrlIndexQuery: String val createUrlIndexQuery: String
@@ -93,7 +90,4 @@ object MangaTable {
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " + "FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " + "ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_ID)" "GROUP BY $TABLE.$COL_ID)"
val addFilteredScanlators: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
} }
@@ -203,15 +203,6 @@ class DownloadManager(private val context: Context) {
deleteChapters(listOf(download.chapter), download.manga, download.source) 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. * Deletes the directories of a list of downloaded chapters.
* *
@@ -272,7 +263,7 @@ class DownloadManager(private val context: Context) {
if (removeNonFavorite && !manga.favorite) { if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(manga, source) val mangaFolder = provider.getMangaDir(manga, source)
cleaned += 1 + mangaFolder.listFiles().orEmpty().size cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
return cleaned return cleaned
@@ -293,7 +284,8 @@ class DownloadManager(private val context: Context) {
if (cache.getDownloadCount(manga) == 0) { if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(manga, source) val mangaFolder = provider.getMangaDir(manga, source)
if (!mangaFolder.listFiles().isNullOrEmpty()) { val size = mangaFolder.listFiles()?.size ?: 0
if (size == 0) {
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
} else { } else {
@@ -28,6 +28,7 @@ internal class DownloadNotifier(private val context: Context) {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false) setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
} }
} }
@@ -83,6 +84,7 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onProgressChange(download: Download) { fun onProgressChange(download: Download) {
with(progressNotificationBuilder) { with(progressNotificationBuilder) {
// Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
clearActions() clearActions()
@@ -114,7 +116,6 @@ internal class DownloadNotifier(private val context: Context) {
} }
setProgress(download.pages!!.size, download.downloadedImages, false) setProgress(download.pages!!.size, download.downloadedImages, false)
setOngoing(true)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS) show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
} }
@@ -129,7 +130,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
setProgress(0, 0, false) setProgress(0, 0, false)
setOngoing(false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query. * @param source the source to query.
*/ */
fun findSourceDir(source: Source): UniFile? { fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source), true) return downloadsDir.findFile(getSourceDirName(source))
} }
/** /**
@@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
*/ */
fun findMangaDir(manga: Manga, source: Source): UniFile? { fun findMangaDir(manga: Manga, source: Source): UniFile? {
val sourceDir = findSourceDir(source) val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga), true) return sourceDir?.findFile(getMangaDirName(manga))
} }
/** /**
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? { fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source) val mangaDir = findMangaDir(manga, source)
return getValidChapterDirNames(chapter).asSequence() return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) } .mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
.firstOrNull() .firstOrNull()
} }
@@ -123,12 +123,14 @@ class DownloadProvider(private val context: Context) {
source: Source source: Source
): List<UniFile> { ): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return mangaDir.listFiles().orEmpty().asList().filter { return mangaDir.listFiles()!!.asList().filter {
chapters.find { chp -> (
getValidChapterDirNames(chp).any { dir -> chapters.find { chp ->
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null getValidChapterDirNames(chp).any { dir ->
} mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true }
} == null
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
} }
} }
// SY <-- // SY <--
@@ -139,7 +141,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query. * @param source the source to query.
*/ */
fun getSourceDirName(source: Source): String { fun getSourceDirName(source: Source): String {
return DiskUtil.buildValidFilename(source.toString()) return source.toString()
} }
/** /**
@@ -176,7 +178,6 @@ class DownloadProvider(private val context: Context) {
return listOf( return listOf(
getChapterDirName(chapter), getChapterDirName(chapter),
// TODO: remove this
// Legacy chapter directory name used in v0.9.2 and before // Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name) DiskUtil.buildValidFilename(chapter.name)
) )
@@ -0,0 +1,60 @@
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
}
}
@@ -0,0 +1,25 @@
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() }
}
}
@@ -0,0 +1,86 @@
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()
}
}
@@ -0,0 +1,15 @@
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)
@@ -0,0 +1,134 @@
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()
}
}
}
@@ -0,0 +1,72 @@
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() {}
}
}
@@ -0,0 +1,55 @@
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,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.CHARGING
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -33,9 +31,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.libraryUpdateInterval().get() val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
if (interval > 0) { if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction().get() val restrictions = preferences.libraryUpdateRestriction()!!
val acRestriction = CHARGING in restrictions val acRestriction = "ac" in restrictions
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) { val wifiRestriction = if ("wifi" in restrictions) {
NetworkType.UNMETERED NetworkType.UNMETERED
} else { } else {
NetworkType.CONNECTED NetworkType.CONNECTED
@@ -6,23 +6,20 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.net.Uri import android.net.Uri
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import coil.imageLoader import com.bumptech.glide.Glide
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop 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.notification
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
@@ -79,8 +76,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.notificationManager.notify( context.notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS, Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder progressNotificationBuilder
.setContentTitle(title.chop(40)) .setContentTitle(title)
.setContentText("($current/$total)")
.setProgress(total, current, false) .setProgress(total, current, false)
.build() .build()
) )
@@ -170,17 +166,14 @@ class LibraryUpdateNotifier(private val context: Context) {
// Per-manga notification // Per-manga notification
if (!preferences.hideNotificationContent()) { if (!preferences.hideNotificationContent()) {
launchUI { updates.forEach { (manga, chapters) ->
updates.forEach { (manga, chapters) -> notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
}
} }
} }
} }
} }
private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification { private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
val icon = getMangaIcon(manga)
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) { return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentTitle(manga.title) setContentTitle(manga.title)
@@ -190,6 +183,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
val icon = getMangaIcon(manga)
if (icon != null) { if (icon != null) {
setLargeIcon(icon) setLargeIcon(icon)
} }
@@ -233,14 +227,23 @@ class LibraryUpdateNotifier(private val context: Context) {
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS) context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
} }
private suspend fun getMangaIcon(manga: Manga): Bitmap? { private fun getMangaIcon(manga: Manga): Bitmap? {
val request = ImageRequest.Builder(context) return try {
.data(manga) Glide.with(context)
.transformations(CircleCropTransformation()) .asBitmap()
.size(NOTIF_ICON_SIZE) .load(manga.toMangaThumbnail())
.build() .dontTransform()
val drawable = context.imageLoader.execute(request).drawable .centerCrop()
return (drawable as? BitmapDrawable)?.bitmap .circleCrop()
.override(
NOTIF_ICON_SIZE,
NOTIF_ICON_SIZE
)
.submit()
.get()
} catch (e: Exception) {
null
}
} }
private fun getNewChaptersDescription(chapters: Array<Chapter>): String { private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
@@ -22,18 +22,16 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager 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.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.library.LibraryGroup import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
@@ -47,7 +45,6 @@ import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isMdBasedSource
import exh.source.mangaDexSourceIds import exh.source.mangaDexSourceIds
import exh.util.executeOnIO import exh.util.executeOnIO
import exh.util.nullIfBlank import exh.util.nullIfBlank
@@ -356,7 +353,6 @@ class LibraryUpdateService(
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>() val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
val failedUpdates = mutableListOf<Pair<Manga, String?>>() val failedUpdates = mutableListOf<Pair<Manga, String?>>()
var hasDownloads = false var hasDownloads = false
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
withIOContext { withIOContext {
mangaToUpdate.groupBy { it.source } mangaToUpdate.groupBy { it.source }
@@ -395,10 +391,6 @@ class LibraryUpdateService(
} }
failedUpdates.add(manga to errorMessage) failedUpdates.add(manga to errorMessage)
} }
if (preferences.autoUpdateTrackers()) {
updateTrackings(manga, loggedServices)
}
} }
} }
} }
@@ -468,7 +460,7 @@ class LibraryUpdateService(
Timber.e(exception) Timber.e(exception)
} }
ioScope.launch(handler) { ioScope.launch(handler) {
if (source.isMdBasedSource() && trackManager.mdList.isLogged) { if (source is MangaDex && trackManager.mdList.isLogged) {
val tracks = db.getTracks(manga).executeOnIO() val tracks = db.getTracks(manga).executeOnIO()
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) { if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
var track = trackManager.mdList.createInitialTracker(manga) var track = trackManager.mdList.createInitialTracker(manga)
@@ -515,7 +507,6 @@ class LibraryUpdateService(
} }
} }
coverCache.clearMemoryCache()
notifier.cancelProgressNotification() notifier.cancelProgressNotification()
} }
@@ -536,35 +527,27 @@ class LibraryUpdateService(
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size) notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
// Update the tracking details. // Update the tracking details.
updateTrackings(manga, loggedServices) db.getTracks(manga).executeAsBlocking()
} .map { track ->
supervisorScope {
notifier.cancelProgressNotification() async {
} val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
private suspend fun updateTrackings(manga: LibraryManga, loggedServices: List<TrackService>) { try {
db.getTracks(manga).executeAsBlocking() val updatedTrack = service.refresh(track)
.map { track -> db.insertTrack(updatedTrack).executeAsBlocking()
supervisorScope { } catch (e: Throwable) {
async { // Ignore errors and continue
val service = trackManager.getService(track.sync_id) Timber.e(e)
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()
.awaitAll() }
notifier.cancelProgressNotification()
} }
// SY --> // SY -->
@@ -577,9 +560,9 @@ class LibraryUpdateService(
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() } val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
val size: Int val size: Int
mangaDex.fetchAllFollows() mangaDex.fetchAllFollows(true)
.filter { (_, metadata) -> .filter { (_, metadata) ->
syncFollowStatusInts.contains(metadata.followStatus) syncFollowStatusInts.contains(metadata.follow_status)
} }
.also { size = it.size } .also { size = it.size }
.forEach { (networkManga, metadata) -> .forEach { (networkManga, metadata) ->
@@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.md.similar.SimilarUpdateService
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -58,22 +59,22 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_SHARE_IMAGE -> ACTION_SHARE_IMAGE ->
shareImage( shareImage(
context, context,
intent.getStringExtra(EXTRA_FILE_LOCATION)!!, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
// Delete image from path and dismiss notification // Delete image from path and dismiss notification
ACTION_DELETE_IMAGE -> ACTION_DELETE_IMAGE ->
deleteImage( deleteImage(
context, context,
intent.getStringExtra(EXTRA_FILE_LOCATION)!!, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
// Share backup file // Share backup file
ACTION_SHARE_BACKUP -> ACTION_SHARE_BACKUP ->
shareFile( shareFile(
context, context,
intent.getParcelableExtra(EXTRA_URI)!!, intent.getParcelableExtra(EXTRA_URI),
"application/x-protobuf+gzip", if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
ACTION_CANCEL_RESTORE -> cancelRestore( ACTION_CANCEL_RESTORE -> cancelRestore(
@@ -106,10 +107,13 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_SHARE_CRASH_LOG -> ACTION_SHARE_CRASH_LOG ->
shareFile( shareFile(
context, context,
intent.getParcelableExtra(EXTRA_URI)!!, intent.getParcelableExtra(EXTRA_URI),
"text/plain", "text/plain",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
// SY -->
ACTION_CANCEL_SIMILAR_UPDATE -> cancelSimilarUpdate(context)
// SY <--
} }
} }
@@ -251,6 +255,18 @@ class NotificationReceiver : BroadcastReceiver() {
} }
} }
// SY -->
/**
* Method called when user wants to stop a similar manga update
*
* @param context context of application
*/
private fun cancelSimilarUpdate(context: Context) {
SimilarUpdateService.stop(context)
Handler().post { dismissNotification(context, Notifications.ID_SIMILAR_PROGRESS) }
}
// SY <--
companion object { companion object {
private const val NAME = "NotificationReceiver" private const val NAME = "NotificationReceiver"
@@ -281,6 +297,12 @@ class NotificationReceiver : BroadcastReceiver() {
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID" 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_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL" private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
// Sy -->
// Called to cancel similar manga update.
private const val ACTION_CANCEL_SIMILAR_UPDATE = "$ID.$NAME.CANCEL_SIMILAR_UPDATE"
// SY <--
/** /**
* Returns a [PendingIntent] that resumes the download of a chapter * Returns a [PendingIntent] that resumes the download of a chapter
@@ -493,10 +515,11 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification * @param notificationId id of notification
* @return [PendingIntent] * @return [PendingIntent]
*/ */
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent { internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply { val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_BACKUP action = ACTION_SHARE_BACKUP
putExtra(EXTRA_URI, uri) putExtra(EXTRA_URI, uri)
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
putExtra(EXTRA_NOTIFICATION_ID, notificationId) putExtra(EXTRA_NOTIFICATION_ID, notificationId)
} }
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
@@ -549,5 +572,20 @@ class NotificationReceiver : BroadcastReceiver() {
} }
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
// SY -->
/**
* Returns [PendingIntent] that starts a service which stops the similar update
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun cancelSimilarUpdatePendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_CANCEL_SIMILAR_UPDATE
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
// SY <--
} }
} }
@@ -68,11 +68,14 @@ object Notifications {
const val CHANNEL_CRASH_LOGS = "crash_logs_channel" const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
const val ID_CRASH_LOGS = -601 const val ID_CRASH_LOGS = -601
// SY -->
/** /**
* Notification channel used for Incognito Mode * Notification channel and ids used for backup and restore.
*/ */
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel" const val CHANNEL_SIMILAR = "similar_channel"
const val ID_INCOGNITO_MODE = -701 const val ID_SIMILAR_PROGRESS = -901
const val ID_SIMILAR_COMPLETE = -902
// SY <--
private val deprecatedChannels = listOf( private val deprecatedChannels = listOf(
"downloader_channel", "downloader_channel",
@@ -162,10 +165,12 @@ object Notifications {
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
), ),
NotificationChannel( NotificationChannel(
CHANNEL_INCOGNITO_MODE, CHANNEL_SIMILAR,
context.getString(R.string.pref_incognito_mode), context.getString(R.string.similar_manga),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) ).apply {
setShowBadge(false)
}
).forEach(context.notificationManager::createNotificationChannel) ).forEach(context.notificationManager::createNotificationChannel)
// Delete old notification channels // Delete old notification channels
@@ -13,9 +13,9 @@ object PreferenceKeys {
const val confirmExit = "pref_confirm_exit" const val confirmExit = "pref_confirm_exit"
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll" const val hideBottomBar = "pref_hide_bottom_bar_on_scroll"
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom" const val rotation = "pref_rotation_type_key"
const val enableTransitionsPager = "pref_enable_transitions_pager_key" const val enableTransitionsPager = "pref_enable_transitions_pager_key"
@@ -53,11 +53,7 @@ object PreferenceKeys {
const val colorFilterMode = "color_filter_mode" const val colorFilterMode = "color_filter_mode"
const val grayscale = "pref_grayscale" const val defaultViewer = "pref_default_viewer_key"
const val defaultReadingMode = "pref_default_reading_mode_key"
const val defaultOrientationType = "pref_default_orientation_type_key"
const val imageScaleType = "pref_image_scale_type_key" const val imageScaleType = "pref_image_scale_type_key"
@@ -101,8 +97,6 @@ object PreferenceKeys {
const val autoUpdateTrack = "pref_auto_update_manga_sync_key" 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 lastUsedSource = "last_catalogue_source"
const val lastUsedCategory = "last_used_category" const val lastUsedCategory = "last_used_category"
@@ -117,8 +111,6 @@ object PreferenceKeys {
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key" const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
const val folderPerManga = "create_folder_per_manga"
const val numberOfBackups = "backup_slots" const val numberOfBackups = "backup_slots"
const val backupInterval = "backup_interval" const val backupInterval = "backup_interval"
@@ -162,7 +154,7 @@ object PreferenceKeys {
const val startScreen = "start_screen" const val startScreen = "start_screen"
const val useAuthenticator = "use_biometric_lock" const val useBiometricLock = "use_biometric_lock"
const val lockAppAfter = "lock_app_after" const val lockAppAfter = "lock_app_after"
@@ -174,8 +166,6 @@ object PreferenceKeys {
const val autoUpdateMetadata = "auto_update_metadata" const val autoUpdateMetadata = "auto_update_metadata"
const val autoUpdateTrackers = "auto_update_trackers"
const val showLibraryUpdateErrors = "show_library_update_errors" const val showLibraryUpdateErrors = "show_library_update_errors"
const val downloadNew = "download_new" const val downloadNew = "download_new"
@@ -199,8 +189,6 @@ object PreferenceKeys {
const val unreadBadge = "display_unread_badge" const val unreadBadge = "display_unread_badge"
const val localBadge = "display_local_badge"
const val categoryTabs = "display_category_tabs" const val categoryTabs = "display_category_tabs"
const val categoryNumberOfItems = "display_number_of_items" const val categoryNumberOfItems = "display_number_of_items"
@@ -225,6 +213,8 @@ object PreferenceKeys {
const val incognitoMode = "incognito_mode" const val incognitoMode = "incognito_mode"
const val createLegacyBackup = "create_legacy_backup"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
@@ -333,6 +323,12 @@ object PreferenceKeys {
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers" const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
const val mangadexSimilarEnabled = "pref_related_show_tab_key"
const val mangadexSimilarUpdateInterval = "related_update_interval"
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes" const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
const val preferredMangaDexId = "preferred_mangaDex_id" const val preferredMangaDexId = "preferred_mangaDex_id"
@@ -357,7 +353,7 @@ object PreferenceKeys {
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders" const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
const val authenticatorTimeRanges = "biometric_time_ranges" const val biometricTimeRanges = "biometric_time_ranges"
const val sortTagsForLibrary = "sort_tags_for_library" const val sortTagsForLibrary = "sort_tags_for_library"
@@ -372,16 +368,4 @@ object PreferenceKeys {
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar" const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
const val forceHorizontalSeekbar = "pref_force_horz_seekbar" const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
const val readerBottomButtons = "reader_bottom_buttons"
const val bottomBarLabels = "pref_show_bottom_bar_labels"
const val hideUpdatesButton = "pref_hide_updates_button"
const val hideHistoryButton = "pref_hide_history_button"
const val pageLayout = "page_layout"
const val invertDoublePages = "invert_double_pages"
} }
@@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.data.preference 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. * This class stores the values for the preferences in the application.
*/ */
@@ -21,19 +18,16 @@ object PreferenceValues {
enum class LightThemeVariant { enum class LightThemeVariant {
default, default,
blue, blue,
strawberrydaiquiri,
} }
// Keys are lowercase to match legacy string values // Keys are lowercase to match legacy string values
enum class DarkThemeVariant { enum class DarkThemeVariant {
default, default,
blue, blue,
greenapple,
midnightdusk,
amoled, amoled,
hotpink,
amoledblue,
red, red,
midnightdusk,
hotpink,
} }
/* ktlint-enable experimental:enum-entry-name-case */ /* ktlint-enable experimental:enum-entry-name-case */
@@ -12,10 +12,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -40,9 +36,8 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item) set(get() - item)
} }
fun Preference<Boolean>.toggle(): Boolean { fun Preference<Boolean>.toggle() {
set(!get()) set(!get())
return get()
} }
class PreferencesHelper(val context: Context) { class PreferencesHelper(val context: Context) {
@@ -66,11 +61,9 @@ class PreferencesHelper(val context: Context) {
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false) fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true) fun hideBottomBar() = flowPrefs.getBoolean(Keys.hideBottomBar, true)
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false) fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0) fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
@@ -82,16 +75,18 @@ class PreferencesHelper(val context: Context) {
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false) fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false) fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
fun clear() = prefs.edit { clear() }
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system) fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default) fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default) fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true) fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true) fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
@@ -128,11 +123,7 @@ class PreferencesHelper(val context: Context) {
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0) fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false) fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
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) fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
@@ -178,8 +169,6 @@ class PreferencesHelper(val context: Context) {
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1) fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0) fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
@@ -216,8 +205,6 @@ class PreferencesHelper(val context: Context) {
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true) fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1) fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0) fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
@@ -230,7 +217,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24) fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK)) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet()) fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
@@ -241,8 +228,6 @@ class PreferencesHelper(val context: Context) {
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false) fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false) fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true) fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
@@ -310,14 +295,16 @@ class PreferencesHelper(val context: Context) {
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL) fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.CHAPTER_SORTING_SOURCE) fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE)
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.CHAPTER_DISPLAY_NAME) fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME)
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC) fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC)
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false) fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
fun setChapterSettingsDefault(manga: Manga) { fun setChapterSettingsDefault(manga: Manga) {
prefs.edit { prefs.edit {
putInt(Keys.defaultChapterFilterByRead, manga.readFilter) putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
@@ -325,7 +312,7 @@ class PreferencesHelper(val context: Context) {
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter) putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting) putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode) putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC) putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC)
} }
} }
@@ -450,8 +437,16 @@ class PreferencesHelper(val context: Context) {
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0") fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
fun mangadexSimilarEnabled() = flowPrefs.getBoolean(Keys.mangadexSimilarEnabled, false)
fun shownMangaDexSimilarAskDialog() = flowPrefs.getBoolean("shown_similar_ask_dialog", false)
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet()) fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false) fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false) fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
@@ -472,7 +467,7 @@ class PreferencesHelper(val context: Context) {
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false) fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
fun authenticatorTimeRanges() = flowPrefs.getStringSet(Keys.authenticatorTimeRanges, mutableSetOf()) fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf()) fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
@@ -487,16 +482,4 @@ class PreferencesHelper(val context: Context) {
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false) fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false) fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
fun readerBottomButtons() = flowPrefs.getStringSet(Keys.readerBottomButtons, ReaderBottomButton.BUTTONS_DEFAULTS)
fun bottomBarLabels() = flowPrefs.getBoolean(Keys.bottomBarLabels, true)
fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false)
fun hideHistoryButton() = flowPrefs.getBoolean(Keys.hideHistoryButton, false)
fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PagerConfig.PageLayout.SINGLE_PAGE)
fun invertDoublePages() = flowPrefs.getBoolean(Keys.invertDoublePages, false)
} }
@@ -1,8 +0,0 @@
package eu.kanade.tachiyomi.data.track
/**
* A TrackService that doesn't need explicit login.
*/
interface NoLoginTrackService {
fun loginNoop()
}
@@ -4,7 +4,6 @@ import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.komga.Komga
import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
@@ -17,10 +16,9 @@ class TrackManager(context: Context) {
const val KITSU = 3 const val KITSU = 3
const val SHIKIMORI = 4 const val SHIKIMORI = 4
const val BANGUMI = 5 const val BANGUMI = 5
const val KOMGA = 6
// SY --> Mangadex from Neko // SY --> Mangadex from Neko
const val MDLIST = 60 const val MDLIST = 6
// SY <-- // SY <--
// SY --> // SY -->
@@ -46,9 +44,7 @@ class TrackManager(context: Context) {
val bangumi = Bangumi(context, BANGUMI) val bangumi = Bangumi(context, BANGUMI)
val komga = Komga(context, KOMGA) val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi, komga)
fun getService(id: Int) = services.find { it.id == id } fun getService(id: Int) = services.find { it.id == id }
@@ -46,6 +46,8 @@ abstract class TrackService(val id: Int) {
abstract fun displayScore(track: Track): String abstract fun displayScore(track: Track): String
abstract suspend fun add(track: Track): Track
abstract suspend fun update(track: Track): Track abstract suspend fun update(track: Track): Track
abstract suspend fun bind(track: Track): Track abstract suspend fun bind(track: Track): Track
@@ -1,21 +0,0 @@
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) {
} }
} }
private suspend fun add(track: Track): Track { override suspend fun add(track: Track): Track {
return api.addLibManga(track) return api.addLibManga(track)
} }
@@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
@@ -28,14 +27,10 @@ import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit.MINUTES
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val authClient = client.newBuilder() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
.addInterceptor(interceptor)
.addInterceptor(RateLimitInterceptor(85, 1, MINUTES))
.build()
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
@@ -31,7 +31,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
private suspend fun add(track: Track): Track { override suspend fun add(track: Track): Track {
return api.addLibManga(track) return api.addLibManga(track)
} }
@@ -67,7 +67,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
return df.format(track.score) return df.format(track.score)
} }
private suspend fun add(track: Track): Track { override suspend fun add(track: Track): Track {
return api.addLibManga(track, getUserId()) return api.addLibManga(track, getUserId())
} }
@@ -1,95 +0,0 @@
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
}
}
@@ -1,84 +0,0 @@
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
}
}
@@ -1,83 +0,0 @@
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,
)
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
@@ -16,12 +17,10 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MdList(private val context: Context, id: Int) : TrackService(id) { class MdList(private val context: Context, id: Int) : TrackService(id) {
private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) } private val mdex by lazy { MdUtil.getEnabledMangaDex() }
@StringRes @StringRes
override fun nameRes(): Int = R.string.mdlist override fun nameRes(): Int = R.string.mdlist
@@ -45,6 +44,8 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override fun displayScore(track: Track) = track.score.toInt().toString() override fun displayScore(track: Track) = track.score.toInt().toString()
override suspend fun add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
return withIOContext { return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException() val mdex = mdex ?: throw MangaDexNotFoundException()
@@ -55,14 +56,11 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
// this updates the follow status in the metadata // this updates the follow status in the metadata
// allow follow status to update // allow follow status to update
if (remoteTrack.status != followStatus.int) { if (remoteTrack.status != followStatus.int) {
if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) { mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
remoteTrack.status = followStatus.int remoteTrack.status = followStatus.int
} else {
track.status = remoteTrack.status
}
} }
/*if (track.score.toInt() > 0) { if (track.score.toInt() > 0) {
mdex.updateRating(track) mdex.updateRating(track)
} }
@@ -84,7 +82,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
} else if (track.last_chapter_read != 0) { } else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does // When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0 track.last_chapter_read = 0
}*/ }
track track
} }
} }
@@ -98,9 +96,9 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
val mdex = mdex ?: throw MangaDexNotFoundException() val mdex = mdex ?: throw MangaDexNotFoundException()
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track) val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
/*if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) { if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0 track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
}*/ }
track track
} }
} }
@@ -138,5 +136,8 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun login(username: String, password: String): Unit = throw Exception("not used") override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
override val isLogged: Boolean
get() = false
class MangaDexNotFoundException : Exception("Mangadex not enabled") class MangaDexNotFoundException : Exception("Mangadex not enabled")
} }
@@ -66,7 +66,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
private suspend fun add(track: Track): Track { override suspend fun add(track: Track): Track {
track.status = READING track.status = READING
track.score = 0F track.score = 0F
return api.updateItem(track) return api.updateItem(track)
@@ -11,6 +11,9 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
private val json: Json by injectLazy() private val json: Json by injectLazy()
private var oauth: OAuth? = null 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 { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@@ -21,19 +24,21 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
if (oauth == null) { if (oauth == null) {
oauth = myanimelist.loadOAuth() oauth = myanimelist.loadOAuth()
} }
// Refresh access token if expired // Refresh access token if null or expired.
if (oauth != null && oauth!!.isExpired()) { if (oauth!!.isExpired()) {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use { chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
if (it.isSuccessful) { if (it.isSuccessful) {
setAuth(json.decodeFromString(it.body!!.string())) setAuth(json.decodeFromString(it.body!!.string()))
} }
} }
} }
// Throw on null auth.
if (oauth == null) { if (oauth == null) {
throw Exception("No authentication token") 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() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build() .build()
@@ -7,9 +7,8 @@ data class OAuth(
val refresh_token: String, val refresh_token: String,
val access_token: String, val access_token: String,
val token_type: String, val token_type: String,
val created_at: Long = System.currentTimeMillis(),
val expires_in: Long val expires_in: Long
) { ) {
fun isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000) fun isExpired() = System.currentTimeMillis() > expires_in
} }
@@ -40,7 +40,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString() return track.score.toInt().toString()
} }
private suspend fun add(track: Track): Track { override suspend fun add(track: Track): Track {
return api.addLibManga(track, getUsername()) return api.addLibManga(track, getUsername())
} }
@@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
@@ -8,26 +11,52 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork() = runBlocking { override fun doWork(): Result {
try { return runBlocking {
val result = GithubUpdateChecker().checkForUpdate() try {
val result = GithubUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) { if (result is UpdateResult.NewUpdate<*>) {
UpdaterNotifier(context).promptUpdate(result.release.downloadLink) val url = result.release.downloadLink
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
)
}
}
Result.success()
} catch (e: Exception) {
Result.failure()
} }
Result.success()
} catch (e: Exception) {
Result.failure()
} }
} }
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object { companion object {
private const val TAG = "UpdateChecker" private const val TAG = "UpdateChecker"
@@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -30,27 +28,6 @@ internal class UpdaterNotifier(private val context: Context) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
fun promptUpdate(url: String) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentIntent(pendingIntent)
clearActions()
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
pendingIntent
)
}
notificationBuilder.show()
}
/** /**
* Call when apk download starts. * Call when apk download starts.
* *
@@ -86,20 +63,19 @@ internal class UpdaterNotifier(private val context: Context) {
* @param uri path location of apk. * @param uri path location of apk.
*/ */
fun onDownloadFinished(uri: Uri) { fun onDownloadFinished(uri: Uri) {
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
with(notificationBuilder) { with(notificationBuilder) {
setContentText(context.getString(R.string.update_check_notification_download_complete)) setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false) setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
setContentIntent(installIntent) // Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
clearActions()
addAction( addAction(
R.drawable.ic_system_update_alt_white_24dp, R.drawable.ic_system_update_alt_white_24dp,
context.getString(R.string.action_install), context.getString(R.string.action_install),
installIntent NotificationHandler.installApkPendingActivity(context, uri)
) )
// Cancel action
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
@@ -120,13 +96,13 @@ internal class UpdaterNotifier(private val context: Context) {
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false) setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Retry action
clearActions()
addAction( addAction(
R.drawable.ic_refresh_24dp, R.drawable.ic_refresh_24dp,
context.getString(R.string.action_retry), context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url) UpdaterService.downloadApkPendingService(context, url)
) )
// Cancel action
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName ($it)") Timber.w(e, "Extension load error: $extName ($it)")
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
@@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
@@ -114,7 +114,10 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
latch.countDown() latch.countDown()
} }
if (url == origRequestUrl && !challengeFound) { // HTTP error codes are only received since M
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
url == origRequestUrl && !challengeFound
) {
// The first request didn't return the challenge, abort. // The first request didn't return the challenge, abort.
latch.countDown() latch.countDown()
} }
@@ -153,7 +156,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
webView?.stopLoading() webView?.stopLoading()
webView?.destroy() webView?.destroy()
webView = null
} }
// Throw exception if we failed to bypass Cloudflare // Throw exception if we failed to bypass Cloudflare
@@ -1,11 +1,8 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import coil.util.CoilUtils
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@@ -21,34 +18,30 @@ import java.util.concurrent.TimeUnit
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val cacheSize = 5L * 1024 * 1024 // 5 MiB
/* SY --> */ open /* SY <-- */val cookieManager = AndroidCookieJar() /* SY --> */ open /* SY <-- */ val cookieManager = AndroidCookieJar()
private val baseClientBuilder: OkHttpClient.Builder /* SY --> */ open /* SY <-- */ val client by lazy {
get() { val builder = OkHttpClient.Builder()
val builder = OkHttpClient.Builder() .cookieJar(cookieManager)
.cookieJar(cookieManager) .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(UserAgentInterceptor()) .addInterceptor(UserAgentInterceptor())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply { val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS 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
} }
/* SY --> */ open /* SY <-- */val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } when (preferences.dohProvider()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
}
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() } builder.build()
}
/* SY --> */ open /* SY <-- */val cloudflareClient by lazy { /* SY --> */ open /* SY <-- */val cloudflareClient by lazy {
client.newBuilder() client.newBuilder()
@@ -9,7 +9,6 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
@@ -71,9 +70,7 @@ suspend fun Call.await(): Response {
return return
} }
continuation.resume(response) { continuation.resume(response)
response.body?.closeQuietly()
}
} }
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Interceptor import okhttp3.Interceptor
@@ -1,58 +0,0 @@
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())
}
}
@@ -1,65 +0,0 @@
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())
}
}
@@ -38,6 +38,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
private const val COVER_NAME = "cover.jpg" private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub") 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) private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
fun updateCover(context: Context, manga: SManga, input: InputStream): File? { fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
@@ -246,10 +248,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
ChapterRecognition.parseChapterNumber(this, manga) ChapterRecognition.parseChapterNumber(this, manga)
} }
} }
.sortedWith { c1, c2 -> .sortedWith(
val c = c2.chapter_number.compareTo(c1.chapter_number) Comparator { c1, c2 ->
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c val c = c2.chapter_number.compareTo(c1.chapter_number)
} if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
}
)
.toList() .toList()
return Observable.just(chapters) return Observable.just(chapters)
@@ -294,7 +298,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
private fun isSupportedFile(extension: String): Boolean { private fun isSupportedFile(extension: String): Boolean {
return extension.toLowerCase(Locale.ROOT) in SUPPORTED_ARCHIVE_TYPES return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
} }
fun getFormat(chapter: SChapter): Format { fun getFormat(chapter: SChapter): Format {
@@ -306,7 +310,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
return getFormat(chapFile) return getFormat(chapFile)
} }
throw Exception(context.getString(R.string.chapter_not_found)) throw Exception("Chapter not found")
} }
private fun getFormat(file: File): Format { private fun getFormat(file: File): Format {
@@ -320,7 +324,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} else if (extension.equals("epub", true)) { } else if (extension.equals("epub", true)) {
Format.Epub(file) Format.Epub(file)
} else { } else {
throw Exception(context.getString(R.string.local_invalid_format)) throw Exception("Invalid chapter format")
} }
} }
@@ -363,16 +367,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
} }
override fun getFilterList() = POPULAR_FILTERS private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
private val POPULAR_FILTERS = FilterList(OrderBy(context)) override fun getFilterList() = FilterList(OrderBy())
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 { sealed class Format {
data class Directory(val file: File) : Format() data class Directory(val file: File) : Format()
@@ -26,14 +26,7 @@ sealed class Filter<T>(val name: String, var state: T) {
} }
// SY --> // SY -->
abstract class AutoComplete( abstract class AutoComplete(name: String, val hint: String, val values: List<String>, val skipAutoFillTags: List<String> = emptyList(), val excludePrefix: String? = null, state: List<String>) : Filter<List<String>>(name, state)
name: String,
val hint: String,
val values: List<String>,
val skipAutoFillTags: List<String> = emptyList(),
val excludePrefix: String? = null,
state: List<String>
) : Filter<List<String>>(name, state)
// SY <-- // SY <--
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -20,10 +20,6 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
return result return result
} }
// SY <-- // SY <--
fun copy(mangas: List<SManga> = this.mangas, hasNextPage: Boolean = this.hasNextPage): MangasPage {
return MangasPage(mangas, hasNextPage)
}
} }
// SY --> // SY -->
@@ -17,7 +17,9 @@ open class Page(
// SY --> // SY -->
var imageUrl = imageUrl var imageUrl = imageUrl
get() = field?.let { DataSaver.compress(it) } get() {
return field?.let { DataSaver.compress(it) }
}
// SY <-- // SY <--
val number: Int val number: Int
@@ -5,5 +5,5 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
interface BrowseSourceFilterHeader : CatalogueSource { interface BrowseSourceFilterHeader : CatalogueSource {
fun getFilterHeader(controller: BaseController<*>, onClick: () -> Unit): RecyclerView.Adapter<*> fun getFilterHeader(controller: BaseController<*>): RecyclerView.Adapter<*>
} }
@@ -8,14 +8,14 @@ import exh.md.utils.FollowStatus
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
interface FollowsSource : CatalogueSource { interface FollowsSource : CatalogueSource {
suspend fun fetchFollows(page: Int): MangasPage suspend fun fetchFollows(): MangasPage
/** /**
* Returns a list of all Follows retrieved by Coroutines * Returns a list of all Follows retrieved by Coroutines
* *
* @param SManga all smanga found for user * @param SManga all smanga found for user
*/ */
suspend fun fetchAllFollows(): List<Pair<SManga, RaisedSearchMetadata>> suspend fun fetchAllFollows(forceHd: Boolean = false): List<Pair<SManga, RaisedSearchMetadata>>
/** /**
* updates the follow status for a manga * updates the follow status for a manga
@@ -1,25 +1,17 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import android.app.Activity
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
interface LoginSource : Source { interface LoginSource : Source {
val requiresLogin: Boolean val needsLogin: Boolean
val twoFactorAuth: AuthSupport
fun isLogged(): Boolean fun isLogged(): Boolean
fun getUsername(): String fun getLoginDialog(source: Source, activity: Activity): DialogController
fun getPassword(): String suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean
suspend fun login(username: String, password: String, twoFactorCode: String?): Boolean
suspend fun logout(): Boolean suspend fun logout(): Boolean
enum class AuthSupport {
NOT_SUPPORTED,
SUPPORTED,
REQUIRED
}
} }
@@ -7,13 +7,11 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.metadata.metadata.base.insertFlatMetadataCompletable
import exh.util.executeOnIO import exh.util.executeOnIO
import rx.Completable import rx.Completable
import rx.Single import rx.Single
@@ -36,7 +34,9 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
/** /**
* Parse the supplied input into the supplied metadata object * Parse the supplied input into the supplied metadata object
*/ */
suspend fun parseIntoMetadata(metadata: M, input: I) fun parseIntoMetadata(metadata: M, input: I)
suspend fun parseInfoIntoMetadata(metadata: M, input: I) = parseIntoMetadata(metadata, input)
/** /**
* Use reflection to create a new instance of metadata * Use reflection to create a new instance of metadata
@@ -51,11 +51,31 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
* *
* Will also save the metadata to the DB if possible * Will also save the metadata to the DB if possible
*/ */
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Use the MangaInfo variant") @Deprecated("Use the MangaInfo variant")
fun parseToManga(manga: SManga, input: I): Completable = runAsObservable({ fun parseToManga(manga: SManga, input: I): Completable {
parseToManga(manga.toMangaInfo(), input) val mangaId = manga.id
}).toCompletable() val metaObservable = if (mangaId != null) {
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
Single.fromCallable {
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
}.map {
it?.raise(metaClass) ?: newMetaInstance()
}
} else {
Single.just(newMetaInstance())
}
return metaObservable.map {
parseIntoMetadata(it, input)
it.copyTo(manga)
it
}.flatMapCompletable {
if (mangaId != null) {
it.mangaId = mangaId
db.insertFlatMetadataCompletable(it.flatten())
} else Completable.complete()
}
}
suspend fun parseToManga(manga: MangaInfo, input: I): MangaInfo { suspend fun parseToManga(manga: MangaInfo, input: I): MangaInfo {
val mangaId = manga.id() val mangaId = manga.id()
@@ -64,7 +84,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
flatMetadata?.raise(metaClass) ?: newMetaInstance() flatMetadata?.raise(metaClass) ?: newMetaInstance()
} else newMetaInstance() } else newMetaInstance()
parseIntoMetadata(metadata, input) parseInfoIntoMetadata(metadata, input)
if (mangaId != null) { if (mangaId != null) {
metadata.mangaId = mangaId metadata.mangaId = mangaId
db.insertFlatMetadata(metadata.flatten()) db.insertFlatMetadata(metadata.flatten())
@@ -80,12 +100,31 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will * If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
* also be saved to the DB. * also be saved to the DB.
*/ */
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("use fetchOrLoadMetadata made for MangaInfo") @Deprecated("use fetchOrLoadMetadata made for MangaInfo")
fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> = fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> {
runAsObservable({ val metaObservable = if (mangaId != null) {
fetchOrLoadMetadata(mangaId) { inputProducer().toObservable().awaitSingle() } // We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
}).toSingle() Single.fromCallable {
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
}.map {
it?.raise(metaClass)
}
} else Single.just(null)
return metaObservable.flatMap { existingMeta ->
if (existingMeta == null) {
inputProducer().flatMap { input ->
val newMeta = newMetaInstance()
parseIntoMetadata(newMeta, input)
val newMetaSingle = Single.just(newMeta)
if (mangaId != null) {
newMeta.mangaId = mangaId
db.insertFlatMetadataCompletable(newMeta.flatten()).andThen(newMetaSingle)
} else newMetaSingle
}
} else Single.just(existingMeta)
}
}
/** /**
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input * Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
@@ -104,7 +143,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
return meta ?: inputProducer().let { input -> return meta ?: inputProducer().let { input ->
val newMeta = newMetaInstance() val newMeta = newMetaInstance()
parseIntoMetadata(newMeta, input) parseInfoIntoMetadata(newMeta, input)
if (mangaId != null) { if (mangaId != null) {
newMeta.mangaId = mangaId newMeta.mangaId = mangaId
db.insertFlatMetadata(newMeta.flatten()).let { newMeta } db.insertFlatMetadata(newMeta.flatten()).let { newMeta }
@@ -16,8 +16,7 @@ import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toChapterInfo
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -25,7 +24,7 @@ import eu.kanade.tachiyomi.source.online.NamespaceSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.awaitSingle
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHTags import exh.eh.EHTags
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
@@ -46,13 +45,13 @@ import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import exh.util.asObservableWithAsyncStacktrace import exh.util.asObservableWithAsyncStacktrace
import exh.util.awaitResponse
import exh.util.dropBlank import exh.util.dropBlank
import exh.util.ignore import exh.util.ignore
import exh.util.nullIfBlank import exh.util.nullIfBlank
import exh.util.trimAll import exh.util.trimAll
import exh.util.trimOrNull import exh.util.trimOrNull
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -76,6 +75,7 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
import rx.Single
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -179,43 +179,38 @@ class EHentai(
tags += parsedTags tags += parsedTags
if (infoElements != null) { if (infoElements != null) {
genre = getGenre(infoElements.getOrNull(1)) getGenre(infoElements.getOrNull(1))?.let { genre = it }
datePosted = getDateTag(infoElements.getOrNull(2)) getDateTag(infoElements.getOrNull(2))?.let { datePosted = it }
averageRating = getRating(infoElements.getOrNull(3)) getRating(infoElements.getOrNull(3))?.let { averageRating = it }
uploader = getUploader(infoElements.getOrNull(4)) getUploader(infoElements.getOrNull(4))?.let { uploader = it }
length = getPageCount(infoElements.getOrNull(5)) getPageCount(infoElements.getOrNull(5))?.let { length = it }
} else { } else {
val parsedGenre = body.selectFirst(".gl1c div") val parsedGenre = body.selectFirst(".gl1c div")
genre = getGenre( getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
genreString = parsedGenre?.text()
?.nullIfBlank()
?.toLowerCase()
?.replace(" ", "")
)
val info = body.selectFirst(".gl2c") val info = body.selectFirst(".gl2c")
val extraInfo = body.selectFirst(".gl4c") val extraInfo = body.selectFirst(".gl4c")
val infoList = info.select("div div") val infoList = info.select("div div")
datePosted = getDateTag(infoList.getOrNull(8)) getDateTag(infoList.getOrNull(8))?.let { datePosted = it }
averageRating = getRating(infoList.getOrNull(9)) getRating(infoList.getOrNull(9))?.let { averageRating = it }
val extraInfoList = extraInfo.select("div") val extraInfoList = extraInfo.select("div")
if (extraInfoList.getOrNull(2) == null) { if (extraInfoList.getOrNull(2) == null) {
uploader = getUploader(extraInfoList.getOrNull(0)) getUploader(extraInfoList.getOrNull(0))?.let { uploader = it }
length = getPageCount(extraInfoList.getOrNull(1)) getPageCount(extraInfoList.getOrNull(1))?.let { length = it }
} else { } else {
uploader = getUploader(extraInfoList.getOrNull(1)) getUploader(extraInfoList.getOrNull(1))?.let { uploader = it }
length = getPageCount(extraInfoList.getOrNull(2)) getPageCount(extraInfoList.getOrNull(2))?.let { length = it }
} }
} }
} }
@@ -289,94 +284,93 @@ class EHentai(
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> = getChapterList(manga) {} override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> = getChapterList(manga) {}
suspend fun getChapterList(manga: MangaInfo, throttleFunc: suspend () -> Unit): List<ChapterInfo> { suspend fun getChapterList(manga: MangaInfo, throttleFunc: () -> Unit) = fetchChapterList(manga.toSManga(), throttleFunc).awaitSingle().map { it.toChapterInfo() }
// Pull all the way to the root gallery
// We can't do this with RxJava or we run into stack overflows on shit like this:
// https://exhentai.org/g/1073061/f9345f1c12/
var url = manga.key
var doc: Document
while (true) {
val gid = EHentaiSearchMetadata.galleryId(url).toInt()
val cachedParent = updateHelper.parentLookupTable.get(
gid
)
if (cachedParent == null) {
throttleFunc()
doc = client.newCall(exGet(baseUrl + url)).await().asJsoup()
val parentLink = doc.select("#gdd .gdt1").find { el ->
el.text().toLowerCase() == "parent:"
}!!.nextElementSibling().selectFirst("a")?.attr("href")
if (parentLink != null) {
updateHelper.parentLookupTable.put(
gid,
GalleryEntry(
EHentaiSearchMetadata.galleryId(parentLink),
EHentaiSearchMetadata.galleryToken(parentLink)
)
)
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
} else break
} else {
this@EHentai.xLogD("Parent cache hit: %s!", gid)
url = EHentaiSearchMetadata.idAndTokenToUrl(
cachedParent.gId,
cachedParent.gToken
)
}
}
val newDisplay = doc.select("#gnd a")
// Build chapter for root gallery
val self = ChapterInfo(
key = EHentaiSearchMetadata.normalizeUrl(doc.location()),
name = "v1: " + doc.selectFirst("#gn").text(),
number = 1f,
dateUpload = MetadataUtil.EX_DATE_FORMAT.parse(
doc.select("#gdd .gdt1").find { el ->
el.text().toLowerCase() == "posted:"
}!!.nextElementSibling().text()
)!!.time
)
// Build and append the rest of the galleries
return if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) {
listOf(self)
} else {
newDisplay.mapIndexed { index, newGallery ->
val link = newGallery.attr("href")
val name = newGallery.text()
val posted = (newGallery.nextSibling() as TextNode).text().removePrefix(", added ")
ChapterInfo(
key = EHentaiSearchMetadata.normalizeUrl(link),
name = "v${index + 2}: $name",
number = index + 2f,
dateUpload = MetadataUtil.EX_DATE_FORMAT.parse(posted)!!.time
)
}.reversed() + self
}
}
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {} override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Use getChapterList instead") @Deprecated("Use getChapterList instead")
fun fetchChapterList(manga: SManga, throttleFunc: suspend () -> Unit) = runAsObservable({ fun fetchChapterList(manga: SManga, throttleFunc: () -> Unit): Observable<List<SChapter>> {
getChapterList(manga.toMangaInfo(), throttleFunc).map { it.toSChapter() } return Single.fromCallable {
}) // Pull all the way to the root gallery
// We can't do this with RxJava or we run into stack overflows on shit like this:
// https://exhentai.org/g/1073061/f9345f1c12/
var url: String = manga.url
var doc: Document? = null
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, baseUrl + chapter.url) runBlocking {
.map { while (true) {
it.mapIndexed { i, s -> val gid = EHentaiSearchMetadata.galleryId(url).toInt()
Page(i, s) val cachedParent = updateHelper.parentLookupTable.get(
gid
)
if (cachedParent == null) {
throttleFunc()
val resp = client.newCall(exGet(baseUrl + url)).execute()
if (!resp.isSuccessful) error("HTTP error (${resp.code})!")
doc = resp.asJsoup()
val parentLink = doc!!.select("#gdd .gdt1").find { el ->
el.text().toLowerCase() == "parent:"
}!!.nextElementSibling().selectFirst("a")?.attr("href")
if (parentLink != null) {
updateHelper.parentLookupTable.put(
gid,
GalleryEntry(
EHentaiSearchMetadata.galleryId(parentLink),
EHentaiSearchMetadata.galleryToken(parentLink)
)
)
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
} else break
} else {
this@EHentai.xLogD("Parent cache hit: %s!", gid)
url = EHentaiSearchMetadata.idAndTokenToUrl(
cachedParent.gId,
cachedParent.gToken
)
}
}
} }
}!!
.doOnNext { pages -> doc!!
if (pages.any { it.url == "https://$domain/img/509.gif" }) throw Exception( }.map { d ->
"Hit page limit" val newDisplay = d.select("#gnd a")
) // Build chapter for root gallery
val self = SChapter.create().apply {
url = EHentaiSearchMetadata.normalizeUrl(d.location())
name = "v1: " + d.selectFirst("#gn").text()
chapter_number = 1f
date_upload = MetadataUtil.EX_DATE_FORMAT.parse(
d.select("#gdd .gdt1").find { el ->
el.text().toLowerCase() == "posted:"
}!!.nextElementSibling().text()
)!!.time
}
// Build and append the rest of the galleries
if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) listOf(self)
else {
newDisplay.mapIndexed { index, newGallery ->
val link = newGallery.attr("href")
val name = newGallery.text()
val posted = (newGallery.nextSibling() as TextNode).text().removePrefix(", added ")
SChapter.create().apply {
this.url = EHentaiSearchMetadata.normalizeUrl(link)
this.name = "v${index + 2}: $name"
this.chapter_number = index + 2f
this.date_upload = MetadataUtil.EX_DATE_FORMAT.parse(posted)!!.time
}
}.reversed() + self
}
}.toObservable()
}
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, baseUrl + chapter.url).map {
it.mapIndexed { i, s ->
Page(i, s)
} }
}!!.doOnNext { pages -> if (pages.any { it.url == "https://$domain/img/509.gif" }) throw Exception("Hit page limit") }
private fun fetchChapterPage( private fun fetchChapterPage(
chapter: SChapter, chapter: SChapter,
@@ -471,23 +465,23 @@ class EHentai(
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request { private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
return GET( return GET(
if (page != null) { page?.let {
addParam(url, "page", (page - 1).toString()) addParam(url, "page", (page - 1).toString())
} else url, } ?: url,
if (additionalHeaders != null) { additionalHeaders?.let { additionalHeadersNotNull ->
val headers = headers.newBuilder() val headers = headers.newBuilder()
additionalHeaders.toMultimap().forEach { (t, u) -> additionalHeadersNotNull.toMultimap().forEach { (t, u) ->
u.forEach { u.forEach {
headers.add(t, it) headers.add(t, it)
} }
} }
headers.build() headers.build()
} else headers } ?: headers
).let { ).let {
if (cache) { if (!cache) {
it
} else {
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build() it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
} else {
it
} }
} }
} }
@@ -535,7 +529,7 @@ class EHentai(
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val exception = Exception("Async stacktrace") val exception = Exception("Async stacktrace")
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).awaitResponse() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
if (response.isSuccessful) { if (response.isSuccessful) {
// Pull to most recent // Pull to most recent
val doc = response.asJsoup() val doc = response.asJsoup()
@@ -563,7 +557,7 @@ class EHentai(
*/ */
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
override suspend fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
with(input) { with(input) {
val url = location() val url = location()
@@ -862,12 +856,13 @@ class EHentai(
private fun combineQuery(filters: FilterList): String { private fun combineQuery(filters: FilterList): String {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter -> val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter ->
filter.state.trimAll().dropBlank().mapNotNull { tag -> val splitState = filter.state.trimAll().dropBlank()
splitState.mapNotNull { tag ->
val split = tag.split(":").filterNot { it.isBlank() } val split = tag.split(":").filterNot { it.isBlank() }
if (split.size > 1) { if (split.size > 1) {
val namespace = split[0].removePrefix("-") val namespace = split[0].removePrefix("-")
val exclude = split[0].startsWith("-") val exclude = split[0].startsWith("-")
AdvSearchEntry(namespace to split[1], exclude) AdvSearchEntry(Pair(namespace, split[1]), exclude)
} else { } else {
null null
} }
@@ -877,7 +872,7 @@ class EHentai(
advSearch.forEach { entry -> advSearch.forEach { entry ->
if (entry.exclude) stringBuilder.append("-") if (entry.exclude) stringBuilder.append("-")
if (entry.search.second.contains(" ")) { if (entry.search.second.contains(" ")) {
stringBuilder.append(("""${entry.search.first}:"${entry.search.second}$"""")) stringBuilder.append(("${entry.search.first}:\"${entry.search.second}$\""))
} else { } else {
stringBuilder.append("${entry.search.first}:${entry.search.second}$") stringBuilder.append("${entry.search.first}:${entry.search.second}$")
} }
@@ -3,8 +3,11 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -19,6 +22,7 @@ import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HitomiDescriptionAdapter import exh.ui.metadata.adapters.HitomiDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@@ -32,17 +36,25 @@ class Hitomi(delegate: HttpSource, val context: Context) :
override val lang = if (id == otherId) "all" else delegate.lang override val lang = if (id == otherId) "all" else delegate.lang
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup()) return parseToManga(manga, response.asJsoup())
} }
override suspend fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
url = input.location() url = input.location()
@@ -56,9 +68,9 @@ class Hitomi(delegate: HttpSource, val context: Context) :
artists = galleryElement.select("h2 a").map { it.text() } artists = galleryElement.select("h2 a").map { it.text() }
tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) } tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
input.select(".gallery-info tr").forEach { galleryInfoElement -> input.select(".gallery-info tr").forEach {
val content = galleryInfoElement.child(1) val content = it.child(1)
when (galleryInfoElement.child(0).text().toLowerCase()) { when (it.child(0).text().toLowerCase()) {
"group" -> { "group" -> {
group = content.text() group = content.text()
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
@@ -91,11 +103,9 @@ class Hitomi(delegate: HttpSource, val context: Context) :
} }
"tags" -> { "tags" -> {
tags += content.select("a").map { tags += content.select("a").map {
val ns = when { val ns = if (it.attr("href").startsWith("/tag/male")) "male"
it.attr("href").startsWith("/tag/male") -> "male" else if (it.attr("href").startsWith("/tag/female")) "female"
it.attr("href").startsWith("/tag/female") -> "female" else "misc"
else -> "misc"
}
RaisedTag( RaisedTag(
ns, ns,
it.text().dropLast(if (ns == "misc") 0 else 2), it.text().dropLast(if (ns == "misc") 0 else 2),
@@ -1,12 +1,19 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@@ -16,10 +23,15 @@ import eu.kanade.tachiyomi.source.online.FollowsSource
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.NamespaceSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.GalleryAddEvent
import exh.GalleryAdder
import exh.md.MangaDexFabHeaderAdapter import exh.md.MangaDexFabHeaderAdapter
import exh.md.handlers.ApiChapterParser import exh.md.handlers.ApiChapterParser
import exh.md.handlers.ApiMangaParser import exh.md.handlers.ApiMangaParser
@@ -27,93 +39,77 @@ import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler import exh.md.handlers.MangaPlusHandler
import exh.md.handlers.SimilarHandler import exh.md.handlers.SimilarHandler
import exh.md.network.MangaDexLoginHelper
import exh.md.network.NoSessionException
import exh.md.network.TokenAuthenticator
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdLang import exh.md.utils.MdLang
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.EOFException
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.reflect.KClass import kotlin.reflect.KClass
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
class MangaDex(delegate: HttpSource, val context: Context) : class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
MetadataSource<MangaDexSearchMetadata, Response>, MetadataSource<MangaDexSearchMetadata, Response>,
// UrlImportableSource, UrlImportableSource,
FollowsSource, FollowsSource,
LoginSource, LoginSource,
BrowseSourceFilterHeader, BrowseSourceFilterHeader,
RandomMangaSource, RandomMangaSource {
NamespaceSource {
override val lang: String = delegate.lang override val lang: String = delegate.lang
override val headers: Headers = super.headers.newBuilder().apply {
add("X-Requested-With", "XMLHttpRequest")
add("Referer", MdUtil.baseUrl)
}.build()
private val mdLang by lazy { private val mdLang by lazy {
MdLang.fromExt(lang) ?: MdLang.ENGLISH MdLang.values().find { it.lang == lang }?.dexLang ?: lang
} }
// override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org") override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
val preferences = Injekt.get<PreferencesHelper>() val preferences: PreferencesHelper by injectLazy()
val mdList: MdList = Injekt.get<TrackManager>().mdList val trackManager: TrackManager by injectLazy()
/*private val sourcePreferences: SharedPreferences by lazy { private val sourcePreferences: SharedPreferences by lazy {
context.getSharedPreferences("source_$id", 0x0000) context.getSharedPreferences("source_$id", 0x0000)
}*/
private val loginHelper = MangaDexLoginHelper(networkHttpClient, preferences, mdList)
override val baseHttpClient: OkHttpClient = super.client.newBuilder()
.authenticator(
TokenAuthenticator(loginHelper)
)
.build()
private fun useLowQualityThumbnail() = false // sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
private val apiMangaParser by lazy {
ApiMangaParser(baseHttpClient, mdLang.lang)
}
private val apiChapterParser by lazy {
ApiChapterParser()
}
private val followsHandler by lazy {
FollowsHandler(baseHttpClient, headers, preferences, mdLang.lang, mdList)
}
private val mangaHandler by lazy {
MangaHandler(baseHttpClient, headers, mdLang.lang, apiMangaParser, followsHandler)
}
private val similarHandler by lazy {
SimilarHandler(baseHttpClient, mdLang.lang)
}
private val mangaPlusHandler by lazy {
MangaPlusHandler(network.client)
} }
/*override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = private fun useLowQualityThumbnail() = sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
importIdToMdId(query) { importIdToMdId(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
}*/ }
/*override suspend fun mapUrlToMangaUrl(uri: Uri): String? { override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") { return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
"/manga/" + uri.pathSegments[1] MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
} else { } else {
null null
} }
@@ -127,44 +123,45 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? { override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? {
val id = uri.pathSegments.getOrNull(2) ?: return null val id = uri.pathSegments.getOrNull(2) ?: return null
val mangaId = MangaHandler(baseHttpClient, headers, mdLang).getMangaIdFromChapterId(id) val mangaId = MangaHandler(client, headers, mdLang).getMangaIdFromChapterId(id)
return MdUtil.mapMdIdToMangaUrl(mangaId) return MdUtil.mapMdIdToMangaUrl(mangaId)
}*/ }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return mangaHandler.fetchMangaDetailsObservable(manga, id, preferences.mangaDexForceLatestCovers().get()) return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
} }
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
return mangaHandler.getMangaDetails(manga, id, preferences.mangaDexForceLatestCovers().get()) return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id)
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return mangaHandler.fetchChapterListObservable(manga) return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
} }
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> { override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return mangaHandler.getChapterList(manga) return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getChapterList(manga)
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return if (chapter.scanlator == "MangaPlus") { return if (chapter.scanlator == "MangaPlus") {
mangaPlusHandler.client.newCall(mangaPlusPageListRequest(chapter)) client.newCall(mangaPlusPageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
val chapterId = apiChapterParser.externalParse(response) val chapterId = ApiChapterParser().externalParse(response)
mangaPlusHandler.fetchPageList(chapterId) MangaPlusHandler(client).fetchPageList(chapterId)
} }
} else super.fetchPageList(chapter) } else super.fetchPageList(chapter)
} }
private fun mangaPlusPageListRequest(chapter: SChapter): Request { private fun mangaPlusPageListRequest(chapter: SChapter): Request {
return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK) val urlChapterId = MdUtil.getChapterId(chapter.url)
return GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
} }
override fun fetchImage(page: Page): Observable<Response> { override fun fetchImage(page: Page): Observable<Response> {
return if (page.imageUrl?.contains("mangaplus", true) == true) { return if (page.imageUrl!!.contains("mangaplus", true)) {
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers)) MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
.asObservableSuccess() .asObservableSuccess()
} else super.fetchImage(page) } else super.fetchImage(page)
} }
@@ -175,98 +172,133 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return MangaDexDescriptionAdapter(controller) return MangaDexDescriptionAdapter(controller)
} }
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
apiMangaParser.parseIntoMetadata(metadata, input, emptyList()) ApiMangaParser(mdLang).parseIntoMetadata(metadata, input, emptyList())
} }
override suspend fun fetchFollows(page: Int): MangasPage { override suspend fun fetchFollows(): MangasPage {
return followsHandler.fetchFollows(page) return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
} }
override val requiresLogin: Boolean = false override val needsLogin: Boolean = true
override val twoFactorAuth = LoginSource.AuthSupport.NOT_SUPPORTED override fun getLoginDialog(source: Source, activity: Activity): DialogController {
return MangadexLoginDialog(source as MangaDex)
}
override fun isLogged(): Boolean { override fun isLogged(): Boolean {
return mdList.isLogged val httpUrl = MdUtil.baseUrl.toHttpUrl()
} return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
override fun getUsername(): String {
return mdList.getUsername()
}
override fun getPassword(): String {
return mdList.getPassword()
} }
override suspend fun login( override suspend fun login(
username: String, username: String,
password: String, password: String,
twoFactorCode: String? twoFactorCode: String
): Boolean { ): Boolean {
val result = loginHelper.login(username, password) return withIOContext {
return if (result is MangaDexLoginHelper.LoginResult.Success) { val formBody = FormBody.Builder().apply {
MdUtil.updateLoginToken(result.token, preferences, mdList) add("login_username", username)
mdList.saveCredentials(username, password) add("login_password", password)
true add("no_js", "1")
} else false add("remember_me", "1")
add("two_factor", twoFactorCode)
}
runCatching {
client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).await().closeQuietly()
}
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
withIOContext { response.body?.string() }.let { jsonData ->
if (jsonData != null) {
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
} else {
throw Exception("Json data was null")
}
}
}
} }
override suspend fun logout(): Boolean { override suspend fun logout(): Boolean {
val result = try { return withIOContext {
loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList)) // https://mangadex.org/ajax/actions.ajax.php?function=logout
} catch (e: NoSessionException) { val httpUrl = MdUtil.baseUrl.toHttpUrl()
true val listOfDexCookies = network.cookieManager.get(httpUrl)
} catch (e: Exception) { val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
e.message?.equals("HTTP error 405") ?: false val token = cookie?.value
} if (token.isNullOrEmpty()) {
return@withIOContext true
}
try {
val result = client.newCall(
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
).await()
val resultStr = withIOContext { result.body?.string() }
if (resultStr?.contains("success", true) == true) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
}
} catch (e: EOFException) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
}
return if (result) { false
mdList.logout() }
true
} else false
} }
override suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> { override suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> {
return followsHandler.fetchAllFollows() return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchAllFollows(forceHd) }
} }
suspend fun updateReadingProgress(track: Track): Boolean { suspend fun updateReadingProgress(track: Track): Boolean {
return followsHandler.updateReadingProgress(track) return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateReadingProgress(track) }
} }
suspend fun updateRating(track: Track): Boolean { suspend fun updateRating(track: Track): Boolean {
return followsHandler.updateRating(track) return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateRating(track) }
} }
override suspend fun fetchTrackingInfo(url: String): Track { override suspend fun fetchTrackingInfo(url: String): Track {
if (!isLogged()) { return withIOContext {
throw Exception("Not Logged in") if (!isLogged()) {
throw Exception("Not Logged in")
}
FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchTrackingInfo(url)
} }
return followsHandler.fetchTrackingInfo(url)
} }
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata?> { suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
return mangaHandler.getTrackingInfo(track) return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail())
} }
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean { override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
return followsHandler.updateFollowStatus(mangaID, followStatus) return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateFollowStatus(mangaID, followStatus) }
} }
override fun getFilterHeader(controller: BaseController<*>, onClick: () -> Unit): MangaDexFabHeaderAdapter { override fun getFilterHeader(controller: BaseController<*>): MangaDexFabHeaderAdapter {
return MangaDexFabHeaderAdapter(controller, this, onClick) return MangaDexFabHeaderAdapter(controller, this)
} }
override suspend fun fetchRandomMangaUrl(): String { override suspend fun fetchRandomMangaUrl(): String {
return mangaHandler.fetchRandomMangaId() return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
} }
suspend fun getMangaSimilar(manga: MangaInfo): MangasPage { fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
return similarHandler.getSimilar(manga) return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
} }
/*private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> = private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
when { when {
query.toIntOrNull() != null -> { query.toIntOrNull() != null -> {
runAsObservable({ runAsObservable({
@@ -284,11 +316,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
} }
else -> fail() else -> fail()
}*/ }
/*companion object { companion object {
private const val REMEMBER_ME = "mangadex_rememberme_token" private const val REMEMBER_ME = "mangadex_rememberme_token"
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault" private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
private const val LOW_QUALITY = 1 private const val LOW_QUALITY = 1
}*/ }
} }
@@ -22,10 +22,6 @@ import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.util.executeOnIO import exh.util.executeOnIO
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
@@ -146,44 +142,33 @@ class MergedSource : HttpSource() {
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted") if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences) val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
var exception: Exception? = null return mangaReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.map {
return supervisorScope { it.load(db, sourceManager)
mangaReferences }.mapNotNull { loadedManga ->
.map { withIOContext {
async { if (loadedManga.manga != null && loadedManga.reference.getChapterUpdates) {
try { loadedManga.source.getChapterList(loadedManga.manga.toMangaInfo())
if (it.mangaSourceId == MERGED_SOURCE_ID) return@async null .map { it.toSChapter() }
val (source, loadedManga, reference) = .let { syncChaptersWithSource(db, it, loadedManga.manga, loadedManga.source) }
it.load(db, sourceManager) .also {
if (loadedManga != null && reference.getChapterUpdates) { if (ifDownloadNewChapters && loadedManga.reference.downloadChapters) {
val chapterList = source.getChapterList(loadedManga.toMangaInfo()) downloadManager.downloadChapters(loadedManga.manga, it.first)
.map { it.toSChapter() }
val results =
syncChaptersWithSource(db, chapterList, loadedManga, source)
if (ifDownloadNewChapters && reference.downloadChapters) {
downloadManager.downloadChapters(
loadedManga,
results.first
)
}
results
} else {
null
} }
} catch (e: Exception) {
if (e is CancellationException) throw e
exception = e
null
} }
} } else {
} null
.awaitAll()
.let { pairs ->
if (exception != null) {
throw exception!!
}
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
} }
}
}.let { pairs ->
val firsts = mutableListOf<Chapter>()
val seconds = mutableListOf<Chapter>()
pairs.forEach {
firsts.addAll(it.first)
seconds.addAll(it.second)
}
firsts to seconds
} }
} }
@@ -3,8 +3,11 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -23,6 +26,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Response import okhttp3.Response
import rx.Observable
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
class NHentai(delegate: HttpSource, val context: Context) : class NHentai(delegate: HttpSource, val context: Context) :
@@ -44,18 +48,32 @@ class NHentai(delegate: HttpSource, val context: Context) :
} }
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response) return parseToManga(manga, response)
} }
override suspend fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) { override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
val json = GALLERY_JSON_REGEX.find(input.body?.string().orEmpty())!!.groupValues[1].replace( val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(
UNICODE_ESCAPE_REGEX UNICODE_ESCAPE_REGEX
) { it.groupValues[1].toInt(radix = 16).toChar().toString() } ) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json) val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json)
@@ -3,8 +3,11 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -20,6 +23,7 @@ import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import rx.Observable
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
class PervEden(delegate: HttpSource, val context: Context) : class PervEden(delegate: HttpSource, val context: Context) :
@@ -30,17 +34,25 @@ class PervEden(delegate: HttpSource, val context: Context) :
override val lang = delegate.lang override val lang = delegate.lang
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup()) return parseToManga(manga, response.asJsoup())
} }
override suspend fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
url = input.location().toUri().path url = input.location().toUri().path
@@ -125,7 +137,7 @@ class PervEden(delegate: HttpSource, val context: Context) :
} }
} }
override suspend fun mapUrlToMangaUrl(uri: Uri): String { override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
val newUri = "http://www.perveden.com/".toUri().buildUpon() val newUri = "http://www.perveden.com/".toUri().buildUpon()
uri.pathSegments.take(3).forEach { uri.pathSegments.take(3).forEach {
newUri.appendPath(it) newUri.appendPath(it)
@@ -3,8 +3,11 @@ package eu.kanade.tachiyomi.source.online.english
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -19,6 +22,7 @@ import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
class EightMuses(delegate: HttpSource, val context: Context) : class EightMuses(delegate: HttpSource, val context: Context) :
@@ -30,11 +34,19 @@ class EightMuses(delegate: HttpSource, val context: Context) :
override val lang = "en" override val lang = "en"
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup()) return parseToManga(manga, response.asJsoup())
@@ -53,7 +65,7 @@ class EightMuses(delegate: HttpSource, val context: Context) :
return SelfContents(selfAlbums, selfImages) return SelfContents(selfAlbums, selfImages)
} }
override suspend fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
path = input.location().toUri().pathSegments path = input.location().toUri().pathSegments
@@ -89,7 +101,7 @@ class EightMuses(delegate: HttpSource, val context: Context) :
"8muses.com" "8muses.com"
) )
override suspend fun mapUrlToMangaUrl(uri: Uri): String { override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
var path = uri.pathSegments.drop(2) var path = uri.pathSegments.drop(2)
if (uri.pathSegments[1].toLowerCase() == "picture") { if (uri.pathSegments[1].toLowerCase() == "picture") {
path = path.dropLast(1) path = path.dropLast(1)
@@ -2,8 +2,11 @@ package eu.kanade.tachiyomi.source.online.english
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -18,6 +21,7 @@ import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
class HBrowse(delegate: HttpSource, val context: Context) : class HBrowse(delegate: HttpSource, val context: Context) :
@@ -29,17 +33,25 @@ class HBrowse(delegate: HttpSource, val context: Context) :
override val lang = "en" override val lang = "en"
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup()) return parseToManga(manga, response.asJsoup())
} }
override suspend fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
val tables = parseIntoTables(input) val tables = parseIntoTables(input)
with(metadata) { with(metadata) {
hbUrl = input.location().removePrefix("$baseUrl/thumbnails") hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
@@ -80,7 +92,7 @@ class HBrowse(delegate: HttpSource, val context: Context) :
) )
override suspend fun mapUrlToMangaUrl(uri: Uri): String? { override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
return uri.pathSegments.firstOrNull()?.let { "/$it/c00001/" } return "/${uri.pathSegments.first()}/c00001/"
} }
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter { override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
@@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.source.online.english
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -52,17 +54,26 @@ class Pururin(delegate: HttpSource, val context: Context) :
} }
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup())
.andThen(Observable.just(manga))
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup()) return parseToManga(manga, response.asJsoup())
} }
override suspend fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) {
val selfLink = input.select("[itemprop=name]").last().parent() val selfLink = input.select("[itemprop=name]").last().parent()
val parsedSelfLink = selfLink.attr("href").toUri().pathSegments val parsedSelfLink = selfLink.attr("href").toUri().pathSegments
with(metadata) { with(metadata) {
prId = parsedSelfLink[parsedSelfLink.lastIndex - 1].toInt() prId = parsedSelfLink[parsedSelfLink.lastIndex - 1].toIntOrNull()
prShortLink = parsedSelfLink.last() prShortLink = parsedSelfLink.last()
val contentWrapper = input.selectFirst(".content-wrapper") val contentWrapper = input.selectFirst(".content-wrapper")
@@ -112,7 +123,7 @@ class Pururin(delegate: HttpSource, val context: Context) :
) )
override suspend fun mapUrlToMangaUrl(uri: Uri): String { override suspend fun mapUrlToMangaUrl(uri: Uri): String {
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments.getOrNull(1)}/${uri.lastPathSegment}" return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
} }
override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter { override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter {
@@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.source.online.english
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
@@ -49,12 +51,20 @@ class Tsumino(delegate: HttpSource, val context: Context) :
return "https://tsumino.com/Book/Info/${uri.lastPathSegment}" return "https://tsumino.com/Book/Info/${uri.lastPathSegment}"
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await() val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup()) return parseToManga(manga, response.asJsoup())
} }
override suspend fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location())!!.toInt() tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location())!!.toInt()
tags.clear() tags.clear()
@@ -1,45 +1,68 @@
package eu.kanade.tachiyomi.ui.base.activity package eu.kanade.tachiyomi.ui.base.activity
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseThemedActivity : AppCompatActivity() { abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) ||
(
themeMode == Values.ThemeMode.system &&
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
)
}
private val lightTheme: Int by lazy {
when (preferences.themeLight().get()) {
Values.LightThemeVariant.blue -> R.style.Theme_Tachiyomi_LightBlue
else -> {
when {
// Light status + navigation bar
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
R.style.Theme_Tachiyomi_Light_Api27
}
// Light status bar + fallback gray navigation bar
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
R.style.Theme_Tachiyomi_Light_Api23
}
// Fallback gray status + navigation bar
else -> {
R.style.Theme_Tachiyomi_Light
}
}
}
}
}
private val darkTheme: Int by lazy {
when (preferences.themeDark().get()) {
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
Values.DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_HotPink
else -> R.style.Theme_Tachiyomi_Dark
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val isDarkMode = when (preferences.themeMode().get()) { setTheme(
ThemeMode.light -> false when {
ThemeMode.dark -> true isDarkMode -> darkTheme
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES else -> lightTheme
}
val themeId = if (isDarkMode) {
when (preferences.themeDark().get()) {
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
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
DarkThemeVariant.amoledblue -> R.style.Theme_Tachiyomi_Amoled_Blue
DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Amoled_Red
} }
} 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)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
} }

Some files were not shown because too many files have changed in this diff Show More