Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 332a631b6c | |||
| 70bef08ed6 | |||
| de05f88d5f | |||
| 89427ff37e | |||
| 63617f3079 | |||
| 0f9f7ffc28 | |||
| 9aab9d4ca4 | |||
| 92ae67630c | |||
| 02b90000f0 | |||
| 6ed5d858aa | |||
| 83bc059573 | |||
| ad39af55d6 | |||
| 8d5b2f40b3 | |||
| 49bee1af91 | |||
| a48508a98e | |||
| 4e3288b2af | |||
| 0f16150613 | |||
| 6dd7491ffe | |||
| 02e6eaae12 | |||
| 73523dbff8 | |||
| 58a503814d | |||
| 7a834ea9f4 | |||
| dcca19e6b8 | |||
| 210fd000b3 | |||
| 3d6f8ddd13 | |||
| a4578611d7 | |||
| bb6932ff80 | |||
| 8dce9a674b | |||
| b93298c411 | |||
| 8928aa77eb | |||
| a6e6fa0099 | |||
| c23edd5b72 | |||
| c8a4ec37e0 | |||
| f62d277894 | |||
| b7efc21ea9 | |||
| 238b2d108d | |||
| bdaf0f7492 | |||
| ced8dc750a | |||
| 035fb9e755 | |||
| 86defec57c | |||
| f20e5d864d | |||
| d83f938e07 | |||
| b4e73cb1eb | |||
| 604c7c703a | |||
| 20021dbf54 | |||
| 281fb9c67b | |||
| 7579bb026f | |||
| 61fb836be4 | |||
| d83361dfe3 | |||
| a5a79c1127 | |||
| 2f0f938d5e | |||
| 750a6c3d11 | |||
| b65305c73e | |||
| 85ef1031b5 | |||
| 69c530dc34 | |||
| ea620a8c74 | |||
| 1fdbae5bf8 | |||
| a1d54880c3 | |||
| d21a652944 | |||
| 444d346874 | |||
| 41b8786415 | |||
| d97805e38b | |||
| eafe3a62e4 | |||
| 0a502fcf31 | |||
| 80960d87f2 | |||
| 166aebdf25 | |||
| b8836b9b6f | |||
| 1d70f0b1dd | |||
| 1240cc5232 | |||
| 0abee585fc | |||
| 4870bb153d | |||
| f2b90bd772 | |||
| d77c65b515 | |||
| 5004e2d62c | |||
| 6e4a0ca1ea | |||
| 883ffaa815 | |||
| 3967a569c4 | |||
| 79e4e3d2a0 | |||
| 4b12e977c0 | |||
| 4333999b85 | |||
| fae290cf22 | |||
| 9ecf83f842 | |||
| f594962731 | |||
| 048eecf655 | |||
| 90253f3bd4 | |||
| 0deb6f6b8d | |||
| 294ade035e | |||
| 64bb34b50d | |||
| 9efb1482f9 | |||
| aff15b3ee2 | |||
| d86f3ffad8 | |||
| 2a3eef0610 | |||
| 3b87111f22 | |||
| b654613345 | |||
| 136b25fb92 | |||
| f3875bda50 | |||
| c41148b465 | |||
| eb0a1668f8 | |||
| f7bc3e0a82 | |||
| 2e4def13e3 | |||
| 9e0e2db25d | |||
| 20aa5b9aa1 | |||
| 6ce4612aa7 | |||
| b51d147986 | |||
| bc896cf605 | |||
| e48f274072 | |||
| a6b98e24dc | |||
| 9a26a3e5a2 | |||
| eeb0f76cce | |||
| bcd36c8fad | |||
| 7f7b2901cb | |||
| 84b9b4db55 | |||
| a5c10dbf28 | |||
| 5fee3ac05a | |||
| d3603a664c | |||
| 7ccaea0d72 | |||
| 921f7aad01 | |||
| bc549c56d6 | |||
| b639e1e4d7 | |||
| 4f877820b2 | |||
| c35e0a0c29 | |||
| 08d11914af | |||
| 309bd83730 | |||
| 6b158cc864 | |||
| 7d82be964c | |||
| 525c3f84e4 | |||
| ef37811020 | |||
| 0d033c7080 |
@@ -2,7 +2,7 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated to the latest version of the app (stable is v1.3.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated to the latest version of the app (stable is v1.3.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated to the latest version of the app (stable is v1.3.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
name: Validate Gradle Wrapper
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: Validation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
@@ -1,5 +1,6 @@
|
||||
name: Issue closer
|
||||
on: [issues]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -31,4 +32,4 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: body
|
||||
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
||||
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
||||
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
name: Pull Request Checker
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
name: Pull request build check
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
apk:
|
||||
name: Generate APK
|
||||
runs-on: ubuntu-18.04
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 1.8
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Get NDK
|
||||
- name: Install NDK
|
||||
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
||||
- name: Build Release APK
|
||||
run: bash ./gradlew assembleDebug --stacktrace
|
||||
- name: Build project
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: TachiyomiSY-${{ github.sha }}.apk
|
||||
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
||||
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
||||
@@ -7,6 +7,7 @@ apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||
// Realm (EH)
|
||||
apply plugin: 'realm-android'
|
||||
@@ -42,8 +43,8 @@ android {
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 6
|
||||
versionName "1.2.0"
|
||||
versionCode 9
|
||||
versionName "1.3.1"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@@ -65,7 +66,6 @@ android {
|
||||
debug {
|
||||
versionNameSuffix "-${getCommitCount()}"
|
||||
applicationIdSuffix ".debug"
|
||||
ext.enableCrashlytics = false
|
||||
}
|
||||
releaseTest {
|
||||
applicationIdSuffix ".rt"
|
||||
@@ -140,11 +140,11 @@ dependencies {
|
||||
|
||||
// AndroidX libraries
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
||||
implementation 'androidx.biometric:biometric:1.1.0-alpha02'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
@@ -152,20 +152,20 @@ dependencies {
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
|
||||
final lifecycle_version = '2.3.0-alpha06'
|
||||
final lifecycle_version = '2.3.0-alpha07'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
|
||||
// Job scheduling
|
||||
final work_version = '2.4.0'
|
||||
final work_version = '2.5.0-alpha01'
|
||||
implementation "androidx.work:work-runtime:$work_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
// UI library
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||
|
||||
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
||||
standardImplementation 'com.google.firebase:firebase-core:17.5.0'
|
||||
|
||||
// ReactiveX
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
@@ -174,14 +174,14 @@ dependencies {
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||
|
||||
// Network client
|
||||
final okhttp_version = '4.8.1'
|
||||
final okhttp_version = '4.9.0'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||
implementation 'com.squareup.okio:okio:2.7.0'
|
||||
implementation 'com.squareup.okio:okio:2.8.0'
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.1'
|
||||
|
||||
// REST
|
||||
final retrofit_version = '2.9.0'
|
||||
@@ -217,7 +217,7 @@ dependencies {
|
||||
implementation 'io.requery:sqlite-android:3.32.2'
|
||||
|
||||
// Preferences
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.1'
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
@@ -277,13 +277,12 @@ dependencies {
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||
|
||||
// Licenses
|
||||
final aboutlibraries_version = '8.3.0'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
||||
// NOTE: REMEMBER TO UPDATE GRADLE PLUGIN
|
||||
implementation 'com.mikepenz:aboutlibraries:8.3.0'
|
||||
|
||||
// Tests
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.assertj:assertj-core:3.12.2'
|
||||
testImplementation 'org.assertj:assertj-core:3.16.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
final robolectric_version = '3.1.4'
|
||||
@@ -291,54 +290,38 @@ dependencies {
|
||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
|
||||
|
||||
// SY for mangadex utils
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0-RC"
|
||||
|
||||
|
||||
final coroutines_version = '1.3.8'
|
||||
final coroutines_version = '1.3.9'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
|
||||
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||
|
||||
// Text distance (EH)
|
||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||
|
||||
// Reprint (EH)
|
||||
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
|
||||
implementation 'com.github.ajalt.reprint:rxjava:3.2.1@aar' // optional: the RxJava 1 interface
|
||||
|
||||
// Swirl (EH)
|
||||
implementation 'com.mattprecious.swirl:swirl:1.2.0'
|
||||
|
||||
// RxJava 2 interop for Realm (EH)
|
||||
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
|
||||
|
||||
// Firebase (EH)
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:17.5.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:17.2.1'
|
||||
|
||||
// Better logging (EH)
|
||||
implementation 'com.elvishew:xlog:1.6.1'
|
||||
|
||||
// Time utils (EH)
|
||||
def typed_time_version = '1.0.2'
|
||||
implementation "com.github.kizitonwose.time:time:$typed_time_version"
|
||||
implementation "com.github.kizitonwose.time:time-android:$typed_time_version"
|
||||
|
||||
// Debug utils (EH)
|
||||
debugImplementation 'com.ms-square:debugoverlay:1.1.3'
|
||||
releaseTestImplementation 'com.ms-square:debugoverlay:1.1.3'
|
||||
releaseImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
||||
testImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
||||
final def debug_overlay_version = '1.1.3'
|
||||
debugImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||
releaseTestImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||
releaseImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
|
||||
testImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
|
||||
|
||||
// Humanize (EH)
|
||||
// Humanize (EH) used for E-Hentai updater statistics
|
||||
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
||||
|
||||
// RatingBar (SY)
|
||||
@@ -346,7 +329,7 @@ dependencies {
|
||||
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
|
||||
final def markwon_version = '4.1.0'
|
||||
final def markwon_version = '4.5.1'
|
||||
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||
@@ -355,16 +338,15 @@ dependencies {
|
||||
implementation "io.noties.markwon:image:$markwon_version"
|
||||
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||
|
||||
implementation 'com.google.guava:guava:27.0.1-android'
|
||||
implementation 'com.google.guava:guava:29.0-android'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.72'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +356,7 @@ repositories {
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||
tasks.withType(AbstractKotlinCompile).all {
|
||||
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
@@ -384,10 +366,10 @@ task copyResources(type: Copy) {
|
||||
include '**/*'
|
||||
}
|
||||
|
||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
||||
preBuild.dependsOn(formatKotlin, copyResources)
|
||||
|
||||
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
// Firebase (EH)
|
||||
apply plugin: 'io.fabric'
|
||||
}
|
||||
// Firebase Crashlytics
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -282,7 +282,7 @@
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- MangaDex -->
|
||||
<!-- <data
|
||||
<data
|
||||
android:host="mangadex.org"
|
||||
android:pathPattern="\/(title|manga)\/"
|
||||
android:scheme="http" />
|
||||
@@ -297,7 +297,7 @@
|
||||
<data
|
||||
android:host="www.mangadex.org"
|
||||
android:pathPattern="\/(title|manga)\/"
|
||||
android:scheme="https" />-->
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
|
||||
@@ -23,7 +23,9 @@ import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.kizitonwose.time.days
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.ms_square.debugoverlay.DebugOverlay
|
||||
import com.ms_square.debugoverlay.modules.FpsModule
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@@ -34,13 +36,9 @@ import exh.debug.DebugToggles
|
||||
import exh.log.CrashlyticsPrinter
|
||||
import exh.log.EHDebugModeOverlay
|
||||
import exh.log.EHLogLevel
|
||||
import exh.syDebugVersion
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import java.io.File
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Security
|
||||
import javax.net.ssl.SSLContext
|
||||
import kotlin.concurrent.thread
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.conscrypt.Conscrypt
|
||||
@@ -49,13 +47,23 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.InjektScope
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
import java.io.File
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Security
|
||||
import javax.net.ssl.SSLContext
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.days
|
||||
|
||||
open class App : Application(), LifecycleObserver {
|
||||
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
setupExhLogging() // EXH logging
|
||||
if (!BuildConfig.DEBUG) addAnalytics()
|
||||
|
||||
workaroundAndroid7BrokenSSL()
|
||||
|
||||
@@ -79,7 +87,6 @@ open class App : Application(), LifecycleObserver {
|
||||
setupNotificationChannels()
|
||||
Realm.init(this)
|
||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||
// Reprint.initialize(this) //Setup fingerprint (EH)
|
||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||
setupDebugOverlay()
|
||||
}
|
||||
@@ -119,6 +126,13 @@ open class App : Application(), LifecycleObserver {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAnalytics() {
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
if (syDebugVersion != "0") {
|
||||
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
@Suppress("unused")
|
||||
fun onAppBackgrounded() {
|
||||
@@ -159,15 +173,14 @@ open class App : Application(), LifecycleObserver {
|
||||
private fun setupExhLogging() {
|
||||
EHLogLevel.init(this)
|
||||
|
||||
val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
|
||||
LogLevel.ALL
|
||||
} else {
|
||||
LogLevel.WARN
|
||||
val logLevel = when {
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
||||
BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||
else -> LogLevel.WARN
|
||||
}
|
||||
|
||||
val logConfig = LogConfiguration.Builder()
|
||||
.logLevel(logLevel)
|
||||
.t()
|
||||
.st(2)
|
||||
.nb()
|
||||
.build()
|
||||
@@ -180,14 +193,17 @@ open class App : Application(), LifecycleObserver {
|
||||
"logs"
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
printers += FilePrinter
|
||||
.Builder(logFolder.absolutePath)
|
||||
.fileNameGenerator(object : DateFileNameGenerator() {
|
||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
|
||||
.fileNameGenerator(
|
||||
object : DateFileNameGenerator() {
|
||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||
}
|
||||
}
|
||||
})
|
||||
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue))
|
||||
)
|
||||
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
||||
.backupStrategy(NeverBackupStrategy())
|
||||
.build()
|
||||
|
||||
@@ -202,6 +218,17 @@ open class App : Application(), LifecycleObserver {
|
||||
)
|
||||
|
||||
XLog.d("Application booting...")
|
||||
XLog.nst().d(
|
||||
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
||||
"Preview build: $syDebugVersion\n" +
|
||||
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
||||
"Android build ID: ${Build.DISPLAY}\n" +
|
||||
"Device brand: ${Build.BRAND}\n" +
|
||||
"Device manufacturer: ${Build.MANUFACTURER}\n" +
|
||||
"Device name: ${Build.DEVICE}\n" +
|
||||
"Device model: ${Build.MODEL}\n" +
|
||||
"Device product name: ${Build.PRODUCT}"
|
||||
)
|
||||
}
|
||||
|
||||
// EXH
|
||||
|
||||
@@ -8,9 +8,9 @@ import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
@@ -36,8 +36,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||
if (interval > 0) {
|
||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||
interval.toLong(), TimeUnit.HOURS,
|
||||
10, TimeUnit.MINUTES
|
||||
interval.toLong(),
|
||||
TimeUnit.HOURS,
|
||||
10,
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
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
|
||||
@@ -32,6 +33,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
@@ -39,6 +41,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.MergedMangaReferenceTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
@@ -57,17 +60,23 @@ import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import exh.EXHSavedSearch
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.eh.EHentaiThrottleManager
|
||||
import java.lang.RuntimeException
|
||||
import kotlin.math.max
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.util.asObservable
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
import java.lang.RuntimeException
|
||||
import kotlin.math.max
|
||||
|
||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
|
||||
@@ -106,6 +115,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
// SY -->
|
||||
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||
// SY <--
|
||||
.create()
|
||||
else -> throw Exception("Json version unknown")
|
||||
}
|
||||
@@ -129,15 +141,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
// 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 {
|
||||
// Get manga from database
|
||||
val mangas = getFavoriteManga()
|
||||
val mangas = getFavoriteManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY --> */ + getMergedManga() /* SY <-- */
|
||||
|
||||
val extensions: MutableSet<String> = mutableSetOf()
|
||||
|
||||
@@ -163,6 +181,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
// SY -->
|
||||
root[SAVEDSEARCHES] =
|
||||
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
||||
|
||||
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -212,6 +232,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun backupMergedMangaReferences(root: JsonArray) {
|
||||
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Backup the categories of library
|
||||
*
|
||||
@@ -317,29 +344,40 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*/
|
||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
// SY -->
|
||||
return (
|
||||
if (source is EHentai) {
|
||||
source.fetchChapterList(manga, throttleManager::throttle)
|
||||
} else {
|
||||
source.fetchChapterList(manga)
|
||||
}
|
||||
).map {
|
||||
if (it.last().chapter_number == -99F) {
|
||||
chapters.forEach { chapter ->
|
||||
chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source"
|
||||
}
|
||||
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
||||
} else {
|
||||
syncChaptersWithSource(databaseHelper, it, manga, source)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
.doOnNext { pair ->
|
||||
if (source is MergedSource) {
|
||||
val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
|
||||
return syncedChapters.onEach { pair ->
|
||||
if (pair.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}.asObservable()
|
||||
} else {
|
||||
return (
|
||||
if (source is EHentai) {
|
||||
source.fetchChapterList(manga, throttleManager::throttle)
|
||||
} else {
|
||||
source.fetchChapterList(manga)
|
||||
}
|
||||
).map {
|
||||
if (it.last().chapter_number == -99F) {
|
||||
chapters.forEach { chapter ->
|
||||
chapter.name =
|
||||
"Chapter ${chapter.chapter_number} restored by dummy source"
|
||||
}
|
||||
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
||||
} else {
|
||||
syncChaptersWithSource(databaseHelper, it, manga, source)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
.doOnNext { pair ->
|
||||
if (pair.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,6 +622,57 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
}
|
||||
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the categories from Json
|
||||
*
|
||||
* @param jsonMergedMangaReferences array containing md manga references
|
||||
*/
|
||||
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
|
||||
// Get merged manga references from file and from db
|
||||
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||
val backupMergedMangaReferences = parser.fromJson<List<MergedMangaReference>>(jsonMergedMangaReferences)
|
||||
var lastMergeManga: Manga? = null
|
||||
|
||||
// Iterate over them
|
||||
backupMergedMangaReferences.forEach { mergedMangaReference ->
|
||||
// Used to know if the merged manga reference is already in the db
|
||||
var found = false
|
||||
for (dbMergedMangaReference in dbMergedMangaReferences) {
|
||||
// If the mergedMangaReference is already in the db, assign the id to the file's mergedMangaReference
|
||||
// and do nothing
|
||||
if (mergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && mergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) {
|
||||
mergedMangaReference.id = dbMergedMangaReference.id
|
||||
mergedMangaReference.mergeId = dbMergedMangaReference.mergeId
|
||||
mergedMangaReference.mangaId = dbMergedMangaReference.mangaId
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// If the mergedMangaReference isn't in the db, remove the id and insert a new mergedMangaReference
|
||||
// Store the inserted id in the mergedMangaReference
|
||||
if (!found) {
|
||||
// Let the db assign the id
|
||||
var mergedManga = if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga
|
||||
if (mergedManga == null) {
|
||||
mergedManga = Manga.create(MERGED_SOURCE_ID).apply {
|
||||
url = mergedMangaReference.mergeUrl
|
||||
title = context.getString(R.string.refresh_merge)
|
||||
}
|
||||
mergedManga.id = databaseHelper.insertManga(mergedManga).executeAsBlocking().insertedId()
|
||||
}
|
||||
|
||||
val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach
|
||||
lastMergeManga = mergedManga
|
||||
|
||||
mergedMangaReference.mergeId = mergedManga.id
|
||||
mergedMangaReference.mangaId = manga.id
|
||||
mergedMangaReference.id = null
|
||||
val result = databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking()
|
||||
mergedMangaReference.id = result.insertedId()
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
@@ -602,6 +691,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
internal fun getFavoriteManga(): List<Manga> =
|
||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
|
||||
internal fun getMergedManga(): List<Manga> =
|
||||
databaseHelper.getMergedMangas().executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
|
||||
@@ -11,9 +11,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class BackupNotifier(private val context: Context) {
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||
@@ -33,14 +34,11 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import exh.EXHMigrations
|
||||
import exh.eh.EHentaiThrottleManager
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -48,6 +46,10 @@ import kotlinx.coroutines.launch
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Restores backup from a JSON file.
|
||||
@@ -238,7 +240,7 @@ class BackupRestoreService : Service() {
|
||||
}
|
||||
|
||||
totalAmount = mangasJson.size()
|
||||
restoreAmount = validManga.count() + 1 // +1 for categories
|
||||
restoreAmount = validManga.count() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
||||
skippedAmount = mangasJson.size() - validManga.count()
|
||||
// SY <--
|
||||
restoreProgress = 0
|
||||
@@ -249,6 +251,8 @@ class BackupRestoreService : Service() {
|
||||
|
||||
// SY -->
|
||||
json.get(SAVEDSEARCHES)?.let { restoreSavedSearches(it) }
|
||||
|
||||
json.get(MERGEDMANGAREFERENCES)?.let { restoreMergedMangaReferences(it) }
|
||||
// SY <--
|
||||
|
||||
// Store source mapping for error messages
|
||||
@@ -288,6 +292,15 @@ class BackupRestoreService : Service() {
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
||||
}
|
||||
|
||||
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
|
||||
db.inTransaction {
|
||||
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.merged_references))
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun restoreManga(mangaJson: JsonObject) {
|
||||
@@ -445,7 +458,12 @@ class BackupRestoreService : Service() {
|
||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
val errorMessage = if (it is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
it.message
|
||||
}
|
||||
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ object Backup {
|
||||
const val VERSION = "version"
|
||||
// SY -->
|
||||
const val SAVEDSEARCHES = "savedsearches"
|
||||
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
||||
// SY <--
|
||||
|
||||
fun getDefaultFilename(): String {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [MergedMangaReference] to / from json
|
||||
*/
|
||||
object MergedMangaReferenceTypeAdapter {
|
||||
|
||||
fun build(): TypeAdapter<MergedMangaReference> {
|
||||
return typeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.mangaUrl)
|
||||
value(it.mergeUrl)
|
||||
value(it.mangaSourceId)
|
||||
value(it.chapterSortMode)
|
||||
value(it.chapterPriority)
|
||||
value(it.getChapterUpdates)
|
||||
value(it.isInfoManga)
|
||||
value(it.downloadChapters)
|
||||
endArray()
|
||||
}
|
||||
|
||||
read {
|
||||
beginArray()
|
||||
MergedMangaReference(
|
||||
id = null,
|
||||
mangaUrl = nextString(),
|
||||
mergeUrl = nextString(),
|
||||
mangaSourceId = nextLong(),
|
||||
chapterSortMode = nextInt(),
|
||||
chapterPriority = nextInt(),
|
||||
getChapterUpdates = nextBoolean(),
|
||||
isInfoManga = nextBoolean(),
|
||||
downloadChapters = nextBoolean(),
|
||||
mangaId = null,
|
||||
mergeId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -22,6 +20,8 @@ import okio.buffer
|
||||
import okio.sink
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Class used to create chapter cache
|
||||
|
||||
@@ -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.MangaQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.merged.sql.queries.MergedQueries
|
||||
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
|
||||
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
||||
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
||||
@@ -36,7 +39,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(context: Context) :
|
||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* EXH --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
|
||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
|
||||
|
||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.name(DbOpenCallback.DATABASE_NAME)
|
||||
@@ -51,11 +54,12 @@ open class DatabaseHelper(context: Context) :
|
||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||
// EXH -->
|
||||
// SY -->
|
||||
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||
// EXH <--
|
||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||
// SY <--
|
||||
.build()
|
||||
|
||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||
|
||||
@@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MergedTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||
import exh.merged.sql.tables.MergedTable
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.metadata.sql.tables.SearchTagTable
|
||||
import exh.metadata.sql.tables.SearchTitleTable
|
||||
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = /* SY --> */ 3 /* SY <-- */
|
||||
const val DATABASE_VERSION = /* SY --> */ 4 /* SY <-- */
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@@ -34,14 +34,12 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(CategoryTable.createTableQuery)
|
||||
execSQL(MangaCategoryTable.createTableQuery)
|
||||
execSQL(HistoryTable.createTableQuery)
|
||||
// EXH -->
|
||||
// SY -->
|
||||
execSQL(SearchMetadataTable.createTableQuery)
|
||||
execSQL(SearchTagTable.createTableQuery)
|
||||
execSQL(SearchTitleTable.createTableQuery)
|
||||
// EXH <--
|
||||
// AZ -->
|
||||
execSQL(MergedTable.createTableQuery)
|
||||
// AZ <--
|
||||
// SY <--
|
||||
|
||||
// DB indexes
|
||||
execSQL(MangaTable.createUrlIndexQuery)
|
||||
@@ -49,17 +47,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||
// EXH -->
|
||||
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||
// EXH <--
|
||||
// AZ -->
|
||||
// SY -->
|
||||
execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||
execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||
execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||
execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||
execSQL(MergedTable.createIndexQuery)
|
||||
// AZ <--
|
||||
// SY <--
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -70,6 +66,11 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
db.execSQL(MangaTable.addDateAdded)
|
||||
db.execSQL(MangaTable.backfillDateAdded)
|
||||
}
|
||||
if (oldVersion < 12) {
|
||||
db.execSQL(MergedTable.dropTableQuery)
|
||||
db.execSQL(MergedTable.createTableQuery)
|
||||
db.execSQL(MergedTable.createIndexQuery)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
|
||||
@@ -5,4 +5,8 @@ class LibraryManga : MangaImpl() {
|
||||
var unread: Int = 0
|
||||
|
||||
var category: Int = 0
|
||||
|
||||
// SY -->
|
||||
var read: Int = 0
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import java.util.Date
|
||||
|
||||
interface ChapterQueries : DbProvider {
|
||||
// SY -->
|
||||
fun getChapters(manga: Manga) = getChaptersByMangaId(manga.id)
|
||||
fun getChapters(manga: Manga) = getChapters(manga.id)
|
||||
|
||||
fun getChaptersByMangaId(mangaId: Long?) = db.get()
|
||||
fun getChapters(mangaId: Long?) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
@@ -27,15 +27,6 @@ interface ChapterQueries : DbProvider {
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChaptersByMergedMangaId(mangaId: Long) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getMergedChaptersQuery(mangaId))
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun getRecentChapters(date: Date) = db.get()
|
||||
@@ -94,6 +85,17 @@ interface ChapterQueries : DbProvider {
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChaptersReadByUrls(urls: List<String>) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
|
||||
.whereArgs(urls.joinToString { "\"$it\"" })
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||
|
||||
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import exh.merged.sql.tables.MergedTable
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
|
||||
interface MangaQueries : DbProvider {
|
||||
@@ -77,15 +78,6 @@ interface MangaQueries : DbProvider {
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun getMergedMangas(id: Long) = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getMergedMangaQuery(id))
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun updateMangaInfo(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaInfoPutResolver())
|
||||
@@ -139,7 +131,7 @@ interface MangaQueries : DbProvider {
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})")
|
||||
.whereArgs(0)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -1,21 +1,48 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MergedTable as Merged
|
||||
import exh.merged.sql.tables.MergedTable as Merged
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Query to get the manga merged into a merged manga
|
||||
*/
|
||||
fun getMergedMangaQuery(id: Long) =
|
||||
fun getMergedMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ?
|
||||
) AS M
|
||||
JOIN ${Manga.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get all the manga that are merged into other manga
|
||||
*/
|
||||
fun getAllMergedMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE}
|
||||
) AS M
|
||||
JOIN ${Manga.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the manga merged into a merged manga using the Url
|
||||
*/
|
||||
fun getMergedMangaFromUrlQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_URL} = ?
|
||||
) AS M
|
||||
JOIN ${Manga.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||
@@ -24,16 +51,15 @@ fun getMergedMangaQuery(id: Long) =
|
||||
/**
|
||||
* Query to get the chapters of all manga in a merged manga
|
||||
*/
|
||||
fun getMergedChaptersQuery(id: Long) =
|
||||
fun getMergedChaptersQuery() =
|
||||
"""
|
||||
SELECT ${Chapter.TABLE}.*
|
||||
FROM (
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
|
||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ?
|
||||
) AS M
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
||||
"""
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories and unread count.
|
||||
@@ -42,23 +68,55 @@ val libraryQuery =
|
||||
"""
|
||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||
FROM (
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
||||
FROM ${Manga.TABLE}
|
||||
LEFT JOIN (
|
||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||
FROM ${Chapter.TABLE}
|
||||
WHERE ${Chapter.COL_READ} = 0
|
||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||
) AS C
|
||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.COL_ID}
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||
FROM ${Manga.TABLE}
|
||||
LEFT JOIN (
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||
FROM ${Chapter.TABLE}
|
||||
WHERE ${Chapter.COL_READ} = 0
|
||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
) AS C
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||
LEFT JOIN (
|
||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
|
||||
FROM ${Chapter.TABLE}
|
||||
WHERE ${Chapter.COL_READ} = 1
|
||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||
) AS R
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
UNION
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||
FROM ${Manga.TABLE}
|
||||
LEFT JOIN (
|
||||
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
|
||||
FROM ${Merged.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 0
|
||||
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
|
||||
) AS C
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
|
||||
LEFT JOIN (
|
||||
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
|
||||
FROM ${Merged.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 1
|
||||
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
|
||||
) AS R
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Merged.COL_MERGE_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} = $MERGED_SOURCE_ID
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER BY ${Manga.COL_TITLE}
|
||||
) AS M
|
||||
LEFT JOIN (
|
||||
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
||||
SELECT * FROM ${MangaCategory.TABLE}
|
||||
) AS MC
|
||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID};
|
||||
"""
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Query to get the recent chapters of manga from the library up to a date.
|
||||
|
||||
@@ -18,6 +18,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
||||
mapBaseFromCursor(manga, cursor)
|
||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
||||
// SY -->
|
||||
manga.read = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_READ))
|
||||
// SY <--
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ object MangaTable {
|
||||
|
||||
const val COL_UNREAD = "unread"
|
||||
|
||||
// SY ->>
|
||||
const val COL_READ = "read"
|
||||
// SY <--
|
||||
|
||||
const val COL_CATEGORY = "category"
|
||||
|
||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MergedTable {
|
||||
|
||||
const val TABLE = "merged"
|
||||
|
||||
const val COL_MERGE_ID = "mergeID"
|
||||
|
||||
const val COL_MANGA_ID = "mangaID"
|
||||
|
||||
val createTableQuery: String
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_MERGE_ID INTEGER NOT NULL,
|
||||
$COL_MANGA_ID INTEGER NOT NULL
|
||||
)"""
|
||||
|
||||
val createIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_MERGE_ID}_index ON $TABLE($COL_MERGE_ID)"
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
||||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files || "$it.cbz" in mangaDir.files }
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -145,7 +145,7 @@ class DownloadCache(
|
||||
mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir.listFiles()
|
||||
.orEmpty()
|
||||
.mapNotNull { it.name }
|
||||
.mapNotNull { it.name?.replace(".cbz", "") }
|
||||
.toHashSet()
|
||||
|
||||
mangaDir.files = chapterDirs
|
||||
@@ -196,6 +196,8 @@ class DownloadCache(
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
} else if ("$it.cbz" in mangaDir.files) {
|
||||
mangaDir.files -= "$it.cbz"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +228,8 @@ class DownloadCache(
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
} else if ("$it.cbz" in mangaDir.files) {
|
||||
mangaDir.files -= "$it.cbz"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,10 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
||||
queue.remove(chapters)
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||
val filteredChapters = getChaptersToDelete(chapters)
|
||||
|
||||
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
||||
chapters.filterNot { it.bookmark }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
queue.remove(filteredChapters)
|
||||
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
@@ -213,9 +209,18 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
|
||||
return filteredChapters
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* return the list of all manga folders
|
||||
*/
|
||||
fun getMangaFolders(source: Source): List<UniFile> {
|
||||
return provider.findSourceDir(source)?.listFiles()?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directories of chapters that were read or have no match
|
||||
*
|
||||
@@ -223,19 +228,39 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source): Int {
|
||||
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int {
|
||||
var cleaned = 0
|
||||
|
||||
if (removeNonFavorite && !manga.favorite) {
|
||||
val mangaFolder = provider.getMangaDir(manga, source)
|
||||
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
|
||||
mangaFolder.delete()
|
||||
cache.removeManga(manga)
|
||||
return cleaned
|
||||
}
|
||||
|
||||
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||
cleaned += filesWithNoChapter.size
|
||||
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||
filesWithNoChapter.forEach { it.delete() }
|
||||
val readChapters = allChapters.filter { it.read }
|
||||
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||
readChapterDirs.forEach { it.delete() }
|
||||
cleaned += readChapterDirs.size
|
||||
cache.removeChapters(readChapters, manga)
|
||||
|
||||
if (removeRead) {
|
||||
val readChapters = allChapters.filter { it.read }
|
||||
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||
readChapterDirs.forEach { it.delete() }
|
||||
cleaned += readChapterDirs.size
|
||||
cache.removeChapters(readChapters, manga)
|
||||
}
|
||||
|
||||
if (cache.getDownloadCount(manga) == 0) {
|
||||
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty
|
||||
val mangaFolder = provider.getMangaDir(manga, source)
|
||||
val size = mangaFolder.listFiles()?.size ?: 0
|
||||
if (size == 0) {
|
||||
mangaFolder.delete()
|
||||
cache.removeManga(manga)
|
||||
} else {
|
||||
Timber.e("Cache and download folder doesn't match for %s", manga.title)
|
||||
}
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
@@ -260,7 +285,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
* @param manga the manga of the chapters.
|
||||
*/
|
||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||
pendingDeleter.addChapters(chapters, manga)
|
||||
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,14 +314,22 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
|
||||
// Assume there's only 1 version of the chapter name formats present
|
||||
val oldFolder = oldNames.asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||
.firstOrNull()
|
||||
|
||||
if (oldFolder?.renameTo(newName) == true) {
|
||||
if (oldFolder?.renameTo(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "") == true) {
|
||||
cache.removeChapter(oldChapter, manga)
|
||||
cache.addChapter(newName, mangaDir, manga)
|
||||
cache.addChapter(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "", mangaDir, manga)
|
||||
} else {
|
||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
||||
return if (!preferences.removeBookmarkedChapters()) {
|
||||
chapters.filterNot { it.bookmark }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||
@@ -107,7 +107,9 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
}
|
||||
|
||||
val downloadingProgressText = context.getString(
|
||||
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
|
||||
R.string.chapter_downloading_progress,
|
||||
download.downloadedImages,
|
||||
download.pages!!.size
|
||||
)
|
||||
|
||||
if (preferences.hideNotificationContent()) {
|
||||
|
||||
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||
val mangaDir = findMangaDir(manga, source)
|
||||
return getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it) }
|
||||
.mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class DownloadProvider(private val context: Context) {
|
||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||
return chapters.mapNotNull { chapter ->
|
||||
getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
@@ -127,10 +127,10 @@ class DownloadProvider(private val context: Context) {
|
||||
(
|
||||
chapters.find { chp ->
|
||||
getValidChapterDirNames(chp).any { dir ->
|
||||
mangaDir.findFile(dir) != null
|
||||
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
||||
}
|
||||
} == null
|
||||
) || it.name?.endsWith("_tmp") == true
|
||||
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
@@ -30,7 +30,13 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
@@ -53,6 +59,8 @@ class Downloader(
|
||||
private val sourceManager: SourceManager
|
||||
) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
|
||||
/**
|
||||
@@ -464,7 +472,39 @@ class Downloader(
|
||||
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (download.status == Download.DOWNLOADED) {
|
||||
tmpDir.renameTo(dirname)
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
val zip = mangaDir.createFile("$dirname.cbz.tmp")
|
||||
val zipOut = ZipOutputStream(BufferedOutputStream(zip.openOutputStream()))
|
||||
val compressionLevel = preferences.saveChaptersAsCBZLevel().get()
|
||||
|
||||
zipOut.setLevel(compressionLevel)
|
||||
|
||||
if (compressionLevel == 0) {
|
||||
zipOut.setMethod(ZipEntry.STORED)
|
||||
}
|
||||
|
||||
tmpDir.listFiles()?.forEach { img ->
|
||||
val input = img.openInputStream()
|
||||
val data = input.readBytes()
|
||||
val entry = ZipEntry(img.name)
|
||||
if (compressionLevel == 0) {
|
||||
val crc = CRC32()
|
||||
val size = img.length()
|
||||
crc.update(data)
|
||||
entry.crc = crc.value
|
||||
entry.compressedSize = size
|
||||
entry.size = size
|
||||
}
|
||||
zipOut.putNextEntry(entry)
|
||||
zipOut.write(data)
|
||||
input.close()
|
||||
}
|
||||
zipOut.close()
|
||||
zip.renameTo("$dirname.cbz")
|
||||
tmpDir.delete()
|
||||
} else {
|
||||
tmpDir.renameTo(dirname)
|
||||
}
|
||||
cache.addChapter(dirname, mangaDir, download.manga)
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
@@ -5,9 +5,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
|
||||
@@ -5,12 +5,12 @@ 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
|
||||
import timber.log.Timber
|
||||
|
||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ 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 java.io.InputStream
|
||||
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.
|
||||
|
||||
@@ -14,9 +14,9 @@ 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 java.io.InputStream
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
|
||||
@@ -29,7 +29,8 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
val json = try {
|
||||
Gson().fromJson(
|
||||
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java
|
||||
Scanner(editJson).useDelimiter("\\Z").next(),
|
||||
JsonObject::class.java
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -83,7 +84,12 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
fun Manga.toJson(): MangaJson {
|
||||
return MangaJson(
|
||||
id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray()
|
||||
id!!,
|
||||
title,
|
||||
author,
|
||||
artist,
|
||||
description,
|
||||
genre?.split(", ")?.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
@@ -45,8 +45,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||
interval.toLong(), TimeUnit.HOURS,
|
||||
10, TimeUnit.MINUTES
|
||||
interval.toLong(),
|
||||
TimeUnit.HOURS,
|
||||
10,
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.addTag(TAG)
|
||||
.setConstraints(constraints)
|
||||
|
||||
@@ -17,14 +17,15 @@ import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
@@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||
fun showProgressNotification(manga: /* SY --> */ SManga /* SY <-- */, current: Int, total: Int) {
|
||||
val title = if (preferences.hideNotificationContent()) {
|
||||
context.getString(R.string.notification_check_updates)
|
||||
} else {
|
||||
@@ -198,18 +199,23 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
// Mark chapters as read action
|
||||
addAction(
|
||||
R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read),
|
||||
R.drawable.ic_glasses_black_24dp,
|
||||
context.getString(R.string.action_mark_as_read),
|
||||
NotificationReceiver.markAsReadPendingBroadcast(
|
||||
context,
|
||||
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
||||
manga,
|
||||
chapters,
|
||||
Notifications.ID_NEW_CHAPTERS
|
||||
)
|
||||
)
|
||||
// View chapters action
|
||||
addAction(
|
||||
R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters),
|
||||
R.drawable.ic_book_24dp,
|
||||
context.getString(R.string.action_view_chapters),
|
||||
NotificationReceiver.openChapterPendingActivity(
|
||||
context,
|
||||
manga, Notifications.ID_NEW_CHAPTERS
|
||||
manga,
|
||||
Notifications.ID_NEW_CHAPTERS
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
@@ -17,10 +19,15 @@ import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
@@ -28,14 +35,25 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
||||
import exh.util.asObservable
|
||||
import exh.util.await
|
||||
import exh.util.awaitSingle
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* This class will take care of updating the chapters of the manga from the library. It can be
|
||||
@@ -72,7 +90,10 @@ class LibraryUpdateService(
|
||||
enum class Target {
|
||||
CHAPTERS, // Manga chapters
|
||||
COVERS, // Manga covers
|
||||
TRACKING // Tracking metadata
|
||||
TRACKING, // Tracking metadata
|
||||
// SY -->
|
||||
SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading
|
||||
// SY <--
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -87,6 +108,14 @@ class LibraryUpdateService(
|
||||
*/
|
||||
const val KEY_TARGET = "target"
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Key for group to update.
|
||||
*/
|
||||
const val KEY_GROUP = "group"
|
||||
const val KEY_GROUP_EXTRA = "group_extra"
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
@@ -106,11 +135,15 @@ class LibraryUpdateService(
|
||||
* @param target defines what should be updated.
|
||||
* @return true if service newly started, false otherwise
|
||||
*/
|
||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
|
||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||
putExtra(KEY_TARGET, target)
|
||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||
// SY -->
|
||||
putExtra(KEY_GROUP, group)
|
||||
groupExtra?.let { putExtra(KEY_GROUP_EXTRA, it) }
|
||||
// SY <--
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
@@ -194,6 +227,9 @@ class LibraryUpdateService(
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.COVERS -> updateCovers(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
// SY -->
|
||||
Target.SYNC_FOLLOWS -> syncFollows()
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -221,10 +257,15 @@ class LibraryUpdateService(
|
||||
*/
|
||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
// SY -->
|
||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
||||
// SY <--
|
||||
|
||||
var listToUpdate = if (categoryId != -1) {
|
||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||
} else {
|
||||
// SY -->
|
||||
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
|
||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||
if (categoriesToUpdate.isNotEmpty()) {
|
||||
db.getLibraryMangas().executeAsBlocking()
|
||||
@@ -233,6 +274,43 @@ class LibraryUpdateService(
|
||||
} else {
|
||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||
}
|
||||
} else {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||
when (group) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val status: String = {
|
||||
val tracks = db.getTracks(it).executeAsBlocking()
|
||||
val track = tracks.find { track ->
|
||||
loggedServices.any { it.id == track?.sync_id }
|
||||
}
|
||||
val service = loggedServices.find { it.id == track?.sync_id }
|
||||
if (track != null && service != null) {
|
||||
service.getStatus(track.status)
|
||||
} else {
|
||||
"not tracked"
|
||||
}
|
||||
}()
|
||||
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
|
||||
}
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> {
|
||||
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
|
||||
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
||||
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
||||
}
|
||||
LibraryGroup.BY_STATUS -> {
|
||||
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
it.status == statusExtra
|
||||
}
|
||||
}
|
||||
LibraryGroup.UNGROUPED -> libraryManga
|
||||
else -> libraryManga
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||
@@ -275,7 +353,12 @@ class LibraryUpdateService(
|
||||
updateManga(manga)
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
failedUpdates.add(Pair(manga, it.message))
|
||||
val errorMessage = if (it is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
it.message
|
||||
}
|
||||
failedUpdates.add(Pair(manga, errorMessage))
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
// Filter out mangas without new chapters (or failed).
|
||||
@@ -328,7 +411,12 @@ class LibraryUpdateService(
|
||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
// We don't want to start downloading while the library is updating, because websites
|
||||
// may don't like it and they could ban the user.
|
||||
downloadManager.downloadChapters(manga, chapters, false)
|
||||
// SY -->
|
||||
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
||||
db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId }
|
||||
} else emptyList()
|
||||
// SY <--
|
||||
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,7 +426,7 @@ class LibraryUpdateService(
|
||||
* @return a pair of the inserted and removed chapters.
|
||||
*/
|
||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
val source = sourceManager.get(manga.source) ?: return Observable.empty()
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
|
||||
// Update manga details metadata in the background
|
||||
if (preferences.autoUpdateMetadata()) {
|
||||
@@ -360,8 +448,38 @@ class LibraryUpdateService(
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
return source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
// SY -->
|
||||
if (source.getMainSource() is MangaDex && trackManager.mdList.isLogged) {
|
||||
try {
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
|
||||
var track = trackManager.mdList.createInitialTracker(manga)
|
||||
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
XLog.e(e)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
return (
|
||||
/* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
|
||||
else /* SY <-- */ source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
// SY -->
|
||||
)
|
||||
.doOnNext {
|
||||
if (source.getMainSource() is MangaDex) {
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
|
||||
var track = trackManager.mdList.createInitialTracker(manga)
|
||||
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
@@ -427,6 +545,48 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
// filter all follows from Mangadex and only add reading or rereading manga to library
|
||||
private fun syncFollows(): Observable<LibraryManga> {
|
||||
val count = AtomicInteger(0)
|
||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager)!!
|
||||
return mangaDex.fetchAllFollows(true)
|
||||
.asObservable()
|
||||
.map { listManga ->
|
||||
listManga.filter { (_, metadata) ->
|
||||
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
||||
}
|
||||
}
|
||||
.doOnNext { listManga ->
|
||||
listManga.forEach { (networkManga, metadata) ->
|
||||
notifier.showProgressNotification(networkManga, count.andIncrement, listManga.size)
|
||||
var dbManga = db.getManga(networkManga.url, mangaDex.id)
|
||||
.executeAsBlocking()
|
||||
if (dbManga == null) {
|
||||
dbManga = Manga.create(
|
||||
networkManga.url,
|
||||
networkManga.title,
|
||||
mangaDex.id
|
||||
)
|
||||
dbManga.date_added = Date().time
|
||||
}
|
||||
|
||||
dbManga.copyFrom(networkManga)
|
||||
dbManga.favorite = true
|
||||
val id = db.insertManga(dbManga).executeAsBlocking().insertedId()
|
||||
if (id != null) {
|
||||
metadata.mangaId = id
|
||||
db.insertFlatMetadata(metadata.flatten()).await()
|
||||
}
|
||||
}
|
||||
}
|
||||
.doOnCompleted {
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
.map { LibraryManga() }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Writes basic file of update errors to cache dir.
|
||||
*/
|
||||
@@ -437,7 +597,8 @@ class LibraryUpdateService(
|
||||
|
||||
destFile.bufferedWriter().use { out ->
|
||||
errors.forEach { (manga, error) ->
|
||||
out.write("${manga.title}: $error\n")
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
out.write("${manga.title} ($source): $error\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@@ -26,10 +25,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import java.io.File
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
/**
|
||||
* Global [BroadcastReceiver] that runs on UI thread
|
||||
@@ -56,19 +56,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Launch share activity and dismiss notification
|
||||
ACTION_SHARE_IMAGE ->
|
||||
shareImage(
|
||||
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// Delete image from path and dismiss notification
|
||||
ACTION_DELETE_IMAGE ->
|
||||
deleteImage(
|
||||
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// Share backup file
|
||||
ACTION_SHARE_BACKUP ->
|
||||
shareBackup(
|
||||
context, intent.getParcelableExtra(EXTRA_URI),
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||
@@ -80,7 +83,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Open reader activity
|
||||
ACTION_OPEN_CHAPTER -> {
|
||||
openChapter(
|
||||
context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||
context,
|
||||
intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,53 +82,62 @@ object Notifications {
|
||||
|
||||
listOf(
|
||||
NotificationChannel(
|
||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||
CHANNEL_COMMON,
|
||||
context.getString(R.string.channel_common),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
||||
CHANNEL_LIBRARY,
|
||||
context.getString(R.string.channel_library),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
||||
CHANNEL_DOWNLOADER_PROGRESS,
|
||||
context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
||||
CHANNEL_DOWNLOADER_COMPLETE,
|
||||
context.getString(R.string.channel_complete),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors),
|
||||
CHANNEL_DOWNLOADER_ERROR,
|
||||
context.getString(R.string.channel_errors),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
||||
CHANNEL_NEW_CHAPTERS,
|
||||
context.getString(R.string.channel_new_chapters),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates),
|
||||
CHANNEL_UPDATES_TO_EXTS,
|
||||
context.getString(R.string.channel_ext_updates),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
|
||||
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
||||
context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
|
||||
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
||||
context.getString(R.string.channel_complete),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
|
||||
@@ -115,6 +115,8 @@ object PreferenceKeys {
|
||||
|
||||
const val filterCompleted = "pref_filter_completed_key"
|
||||
|
||||
const val filterStarted = "pref_filter_started_key"
|
||||
|
||||
const val filterTracked = "pref_filter_tracked_key"
|
||||
|
||||
const val filterLewd = "pref_filter_lewd_key"
|
||||
@@ -280,4 +282,40 @@ object PreferenceKeys {
|
||||
const val startReadingButton = "start_reading_button"
|
||||
|
||||
const val groupLibraryBy = "group_library_by"
|
||||
|
||||
const val continuousVerticalTappingByPage = "continuous_vertical_tapping_by_page"
|
||||
|
||||
const val groupLibraryUpdateType = "group_library_update_type"
|
||||
|
||||
const val useNewSourceNavigation = "use_new_source_navigation"
|
||||
|
||||
const val mangaDexLowQualityCovers = "manga_dex_low_quality_covers"
|
||||
|
||||
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
||||
|
||||
const val preferredMangaDexId = "preferred_mangaDex_id"
|
||||
|
||||
const val dataSaver = "data_saver"
|
||||
|
||||
const val ignoreJpeg = "ignore_jpeg"
|
||||
|
||||
const val ignoreGif = "ignore_gif"
|
||||
|
||||
const val dataSaverImageQuality = "data_saver_image_quality"
|
||||
|
||||
const val dataSaverImageFormatJpeg = "data_saver_image_format_jpeg"
|
||||
|
||||
const val dataSaverServer = "data_saver_server"
|
||||
|
||||
const val dataSaverColorBW = "data_saver_color_bw"
|
||||
|
||||
const val saveChaptersAsCBZ = "save_chapter_as_cbz"
|
||||
|
||||
const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level"
|
||||
|
||||
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
||||
|
||||
const val biometricTimeRanges = "biometric_time_ranges"
|
||||
|
||||
const val sortTagsForLibrary = "sort_tags_for_library"
|
||||
}
|
||||
|
||||
@@ -23,11 +23,15 @@ object PreferenceValues {
|
||||
default,
|
||||
blue,
|
||||
amoled,
|
||||
red,
|
||||
}
|
||||
|
||||
enum class DisplayMode {
|
||||
COMPACT_GRID,
|
||||
COMFORTABLE_GRID,
|
||||
// SY -->
|
||||
NO_TITLE_GRID,
|
||||
// SY <--
|
||||
LIST,
|
||||
}
|
||||
|
||||
@@ -43,4 +47,12 @@ object PreferenceValues {
|
||||
PARTIAL,
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
// SY -->
|
||||
enum class GroupLibraryMode {
|
||||
GLOBAL,
|
||||
ALL_BUT_UNGROUPED,
|
||||
ALL
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ import androidx.preference.PreferenceManager
|
||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||
@@ -215,6 +215,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0)
|
||||
|
||||
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, 0)
|
||||
|
||||
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0)
|
||||
|
||||
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0)
|
||||
@@ -354,7 +356,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
|
||||
|
||||
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
||||
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 10)
|
||||
|
||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
||||
|
||||
@@ -385,4 +387,40 @@ class PreferencesHelper(val context: Context) {
|
||||
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
||||
|
||||
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
|
||||
|
||||
fun continuousVerticalTappingByPage() = flowPrefs.getBoolean(Keys.continuousVerticalTappingByPage, false)
|
||||
|
||||
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
||||
|
||||
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, true)
|
||||
|
||||
fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false)
|
||||
|
||||
fun mangaDexForceLatestCovers() = flowPrefs.getBoolean(Keys.mangaDexForceLatestCovers, false)
|
||||
|
||||
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
||||
|
||||
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||
|
||||
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
||||
|
||||
fun ignoreGif() = flowPrefs.getBoolean(Keys.ignoreGif, true)
|
||||
|
||||
fun dataSaverImageQuality() = flowPrefs.getInt(Keys.dataSaverImageQuality, 80)
|
||||
|
||||
fun dataSaverImageFormatJpeg() = flowPrefs.getBoolean(Keys.dataSaverImageFormatJpeg, false)
|
||||
|
||||
fun dataSaverServer() = flowPrefs.getString(Keys.dataSaverServer, "")
|
||||
|
||||
fun dataSaverColorBW() = flowPrefs.getBoolean(Keys.dataSaverColorBW, false)
|
||||
|
||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean(Keys.saveChaptersAsCBZ, false)
|
||||
|
||||
fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0)
|
||||
|
||||
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
||||
|
||||
fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
|
||||
|
||||
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
|
||||
@@ -15,8 +16,24 @@ class TrackManager(context: Context) {
|
||||
const val KITSU = 3
|
||||
const val SHIKIMORI = 4
|
||||
const val BANGUMI = 5
|
||||
|
||||
// SY --> Mangadex from Neko
|
||||
const val MDLIST = 6
|
||||
// SY <--
|
||||
|
||||
// SY -->
|
||||
const val READING = 1
|
||||
const val REREADING = 2
|
||||
const val PLANTOREAD = 3
|
||||
const val PAUSED = 4
|
||||
const val COMPLETED = 5
|
||||
const val DROPPED = 6
|
||||
const val OTHER = 7
|
||||
// SY <--
|
||||
}
|
||||
|
||||
val mdList = MdList(context, MDLIST)
|
||||
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
|
||||
val aniList = Anilist(context, ANILIST)
|
||||
@@ -27,9 +44,25 @@ class TrackManager(context: Context) {
|
||||
|
||||
val bangumi = Bangumi(context, BANGUMI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
fun hasLoggedServices() = services.any { it.isLogged }
|
||||
fun hasLoggedServices(isMangaDexManga: Boolean = true) = services.any { it.isLogged && ((it.id == MDLIST && isMangaDexManga) || it.id != MDLIST) }
|
||||
|
||||
// SY -->
|
||||
fun mapTrackingOrder(status: String, context: Context): Int {
|
||||
with(context) {
|
||||
return when (status) {
|
||||
getString(eu.kanade.tachiyomi.R.string.reading), getString(eu.kanade.tachiyomi.R.string.currently_reading) -> READING
|
||||
getString(eu.kanade.tachiyomi.R.string.repeating) -> REREADING
|
||||
getString(eu.kanade.tachiyomi.R.string.plan_to_read), getString(eu.kanade.tachiyomi.R.string.want_to_read) -> PLANTOREAD
|
||||
getString(eu.kanade.tachiyomi.R.string.on_hold), getString(eu.kanade.tachiyomi.R.string.paused) -> PAUSED
|
||||
getString(eu.kanade.tachiyomi.R.string.completed) -> COMPLETED
|
||||
getString(eu.kanade.tachiyomi.R.string.dropped) -> DROPPED
|
||||
else -> OTHER
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import java.util.Calendar
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import java.util.Calendar
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
@@ -271,9 +271,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
|
||||
return ALManga(
|
||||
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
||||
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(),
|
||||
date, struct["chapters"].nullInt ?: 0
|
||||
struct["id"].asInt,
|
||||
struct["title"]["romaji"].asString,
|
||||
struct["coverImage"]["large"].asString,
|
||||
struct["description"].nullString.orEmpty(),
|
||||
struct["type"].asString,
|
||||
struct["status"].nullString.orEmpty(),
|
||||
date,
|
||||
struct["chapters"].nullInt ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
data class ALManga(
|
||||
val media_id: Int,
|
||||
|
||||
@@ -12,13 +12,13 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import java.net.URLEncoder
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import java.text.DecimalFormat
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package eu.kanade.tachiyomi.data.track.mdlist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.util.asObservable
|
||||
import exh.util.floor
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
override val name = "MDList"
|
||||
|
||||
override fun getLogo(): Int {
|
||||
return R.drawable.ic_tracker_mangadex_logo
|
||||
}
|
||||
|
||||
override fun getLogoColor(): Int {
|
||||
return Color.rgb(43, 48, 53)
|
||||
}
|
||||
|
||||
override fun getStatusList(): List<Int> {
|
||||
return FollowStatus.values().map { it.int }
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String =
|
||||
context.resources.getStringArray(R.array.md_follows_options).asList()[status]
|
||||
|
||||
override fun getScoreList() = IntRange(0, 10).map(Int::toString)
|
||||
|
||||
override fun displayScore(track: Track) = track.score.toInt().toString()
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return update(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||
return Observable.defer {
|
||||
db.getManga(track.tracking_url.substringAfter(".org"), mdex.id)
|
||||
.asRxObservable()
|
||||
.map { manga ->
|
||||
val mangaMetadata = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise(MangaDexSearchMetadata::class) ?: throw Exception("Invalid manga metadata")
|
||||
val followStatus = FollowStatus.fromInt(track.status)!!
|
||||
|
||||
// allow follow status to update
|
||||
if (mangaMetadata.follow_status != followStatus.int) {
|
||||
runBlocking { mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus).collect() }
|
||||
mangaMetadata.follow_status = followStatus.int
|
||||
db.insertFlatMetadata(mangaMetadata.flatten()).await()
|
||||
}
|
||||
|
||||
if (track.score.toInt() > 0) {
|
||||
runBlocking { mdex.updateRating(track).collect() }
|
||||
}
|
||||
|
||||
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||
|
||||
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = FollowStatus.COMPLETED.int
|
||||
}
|
||||
|
||||
runBlocking { mdex.updateReadingProgress(track).collect() }
|
||||
} else if (track.last_chapter_read != 0) {
|
||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||
track.last_chapter_read = 0
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
|
||||
.doOnNext { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = if (remoteTrack.total_chapters == 0) {
|
||||
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
|
||||
} else {
|
||||
remoteTrack.total_chapters
|
||||
}
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = if (remoteTrack.total_chapters == 0) {
|
||||
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
|
||||
} else {
|
||||
remoteTrack.total_chapters
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun createInitialTracker(manga: Manga): Track {
|
||||
val track = Track.create(TrackManager.MDLIST)
|
||||
track.manga_id = manga.id!!
|
||||
track.status = FollowStatus.UNFOLLOWED.int
|
||||
track.tracking_url = MdUtil.baseUrl + manga.url
|
||||
track.title = manga.title
|
||||
return track
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> = throw Exception("not used")
|
||||
|
||||
override fun login(username: String, password: String): Completable = throw Exception("not used")
|
||||
}
|
||||
@@ -11,13 +11,6 @@ import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.lang.toCalendar
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.zip.GZIPInputStream
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -30,6 +23,13 @@ import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.parser.Parser
|
||||
import rx.Observable
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||
|
||||
abstract class UpdateChecker {
|
||||
|
||||
companion object {
|
||||
fun getUpdateChecker(): UpdateChecker {
|
||||
// SY -->
|
||||
return GithubUpdateChecker()
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns observable containing release information
|
||||
*/
|
||||
abstract suspend fun checkForUpdate(): UpdateResult
|
||||
}
|
||||
@@ -13,9 +13,10 @@ import androidx.work.Worker
|
||||
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.util.system.notificationManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
@@ -23,7 +24,7 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
override fun doWork(): Result {
|
||||
return runBlocking {
|
||||
try {
|
||||
val result = UpdateChecker.getUpdateChecker().checkForUpdate()
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
val url = result.release.downloadLink
|
||||
@@ -65,8 +66,10 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
||||
3, TimeUnit.DAYS,
|
||||
3, TimeUnit.HOURS
|
||||
3,
|
||||
TimeUnit.DAYS,
|
||||
3,
|
||||
TimeUnit.HOURS
|
||||
)
|
||||
.addTag(TAG)
|
||||
.setConstraints(constraints)
|
||||
|
||||
@@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import java.io.File
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
|
||||
class UpdaterService : Service() {
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.Release
|
||||
|
||||
class DevRepoRelease(override val info: String) : Release {
|
||||
|
||||
override val downloadLink: String
|
||||
get() = LATEST_URL
|
||||
|
||||
companion object {
|
||||
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DevRepoUpdateChecker : UpdateChecker() {
|
||||
|
||||
private val client: OkHttpClient by lazy {
|
||||
Injekt.get<NetworkHelper>().client.newBuilder()
|
||||
.followRedirects(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val versionRegex: Regex by lazy {
|
||||
Regex("tachiyomi-r(\\d+).apk")
|
||||
}
|
||||
|
||||
override suspend fun checkForUpdate(): UpdateResult {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
client.newCall(GET(DevRepoRelease.LATEST_URL)).await()
|
||||
}
|
||||
|
||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
||||
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
|
||||
|
||||
return if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
|
||||
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
|
||||
} else {
|
||||
DevRepoUpdateResult.NoNewUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
|
||||
sealed class DevRepoUpdateResult : UpdateResult() {
|
||||
|
||||
class NewUpdate(release: DevRepoRelease) : UpdateResult.NewUpdate<DevRepoRelease>(release)
|
||||
class NoNewUpdate : UpdateResult.NoNewUpdate()
|
||||
}
|
||||
@@ -28,5 +28,5 @@ class GithubRelease(
|
||||
* Assets class containing download url.
|
||||
* @param downloadLink download url.
|
||||
*/
|
||||
inner class Assets(@SerializedName("browser_download_url") val downloadLink: String)
|
||||
class Assets(@SerializedName("browser_download_url") val downloadLink: String)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Used to connect with the GitHub API.
|
||||
* Used to connect with the GitHub API to get the latest release version from a repo.
|
||||
*/
|
||||
interface GithubService {
|
||||
|
||||
@@ -24,11 +25,6 @@ interface GithubService {
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@GET("/repos/jobobby04/tachiyomiSY/releases/latest")
|
||||
suspend fun getLatestVersion(): GithubRelease
|
||||
|
||||
@GET("/repos/jobobby04/TachiyomiSYPreview/releases/latest")
|
||||
suspend fun getLatestDebugVersion(): GithubRelease
|
||||
// SY <--
|
||||
@GET("/repos/{repo}/releases/latest")
|
||||
suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease
|
||||
}
|
||||
|
||||
@@ -1,28 +1,49 @@
|
||||
package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import exh.syDebugVersion
|
||||
// SY -->
|
||||
class GithubUpdateChecker(val debug: Boolean = false) : UpdateChecker() {
|
||||
|
||||
class GithubUpdateChecker {
|
||||
|
||||
private val service: GithubService = GithubService.create()
|
||||
|
||||
override suspend fun checkForUpdate(): UpdateResult {
|
||||
val release = if (syDebugVersion != "0") {
|
||||
service.getLatestDebugVersion()
|
||||
private val repo: String by lazy {
|
||||
// Sy -->
|
||||
if (syDebugVersion != "0") {
|
||||
"jobobby04/TachiyomiSYPreview"
|
||||
} else {
|
||||
service.getLatestVersion()
|
||||
"jobobby04/tachiyomiSY"
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
suspend fun checkForUpdate(): UpdateResult {
|
||||
val release = service.getLatestVersion(repo)
|
||||
|
||||
val newVersion = release.version
|
||||
// Check if latest version is different from current version
|
||||
// SY -->
|
||||
val newVersion = release.version
|
||||
return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) {
|
||||
// SY <--
|
||||
GithubUpdateResult.NewUpdate(release)
|
||||
} else {
|
||||
GithubUpdateResult.NoNewUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNewVersion(versionTag: String): Boolean {
|
||||
// Removes prefixes like "r" or "v"
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
return if (BuildConfig.DEBUG) {
|
||||
// Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo
|
||||
// tagged as something like "r1234"
|
||||
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
|
||||
} else {
|
||||
// Release builds: based on releases in "inorichi/tachiyomi" repo
|
||||
// tagged as something like "v0.1.2"
|
||||
newVersion != BuildConfig.VERSION_NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -16,10 +16,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
@@ -73,8 +73,10 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
|
||||
12, TimeUnit.HOURS,
|
||||
1, TimeUnit.HOURS
|
||||
12,
|
||||
TimeUnit.HOURS,
|
||||
1,
|
||||
TimeUnit.HOURS
|
||||
)
|
||||
.addTag(TAG)
|
||||
.setConstraints(constraints)
|
||||
|
||||
@@ -10,10 +10,10 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import exh.source.BlacklistedSources
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
|
||||
@@ -178,7 +178,13 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val extension = Extension.Installed(
|
||||
extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
|
||||
extName,
|
||||
pkgName,
|
||||
versionName,
|
||||
versionCode,
|
||||
lang,
|
||||
isNsfw,
|
||||
sources,
|
||||
isUnofficial = signatureHash != officialSignature
|
||||
)
|
||||
return LoadResult.Success(extension)
|
||||
|
||||
@@ -16,15 +16,15 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
@@ -89,7 +89,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
var isWebViewOutdated = false
|
||||
|
||||
val origRequestUrl = request.url.toString()
|
||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
|
||||
@@ -3,15 +3,15 @@ package eu.kanade.tachiyomi.network
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.Cache
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
@@ -13,6 +9,10 @@ import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.unsafeCreate { subscriber ->
|
||||
@@ -54,22 +54,24 @@ fun Call.asObservable(): Observable<Response> {
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (assertSuccess && !response.isSuccessful) {
|
||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
return
|
||||
enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (assertSuccess && !response.isSuccessful) {
|
||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
continuation.resume(response)
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// Don't bother with resuming the continuation if it is already cancelled.
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// Don't bother with resuming the continuation if it is already cancelled.
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import java.io.IOException
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Buffer
|
||||
@@ -8,6 +7,7 @@ import okio.BufferedSource
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
@@ -15,6 +16,12 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import junrar.Archive
|
||||
import junrar.rarfile.FileHeader
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
@@ -22,10 +29,6 @@ import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import junrar.Archive
|
||||
import junrar.rarfile.FileHeader
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource {
|
||||
companion object {
|
||||
@@ -74,6 +77,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
// SY -->
|
||||
val allowLocalSourceHiddenFolders = Injekt.get<PreferencesHelper>().allowLocalSourceHiddenFolders().get()
|
||||
// SY <--
|
||||
|
||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
var mangaDirs = baseDirs
|
||||
@@ -81,6 +87,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
.mapNotNull { it.listFiles()?.toList() }
|
||||
.flatten()
|
||||
.filter { it.isDirectory }
|
||||
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
|
||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
.distinctBy { it.name }
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||
@@ -31,13 +32,13 @@ import exh.TSUMINO_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.source.EnhancedHttpSource
|
||||
import kotlin.reflect.KClass
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
@@ -191,14 +192,14 @@ open class SourceManager(private val context: Context) {
|
||||
TSUMINO_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||
Tsumino::class
|
||||
)/*,
|
||||
),
|
||||
DelegatedSource(
|
||||
"MangaDex",
|
||||
fillInSourceId,
|
||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||
MangaDex::class,
|
||||
true
|
||||
)*/,
|
||||
),
|
||||
DelegatedSource(
|
||||
"HBrowse",
|
||||
HBROWSE_SOURCE_ID,
|
||||
|
||||
@@ -2,16 +2,26 @@ package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import exh.util.DataSaver
|
||||
import rx.subjects.Subject
|
||||
|
||||
open class Page(
|
||||
val index: Int,
|
||||
/* SY --> */
|
||||
var /* SY <-- */ url: String = "",
|
||||
var imageUrl: String? = null,
|
||||
/* SY --> var <-- SY */
|
||||
imageUrl: String? = null,
|
||||
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||
) : ProgressListener {
|
||||
|
||||
// SY -->
|
||||
var imageUrl = imageUrl
|
||||
get() {
|
||||
if (field == null) return null
|
||||
return DataSaver().compress(field!!)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val number: Int
|
||||
get() = index + 1
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ interface SManga : Serializable {
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
// SY --> Mangadex specific statuses
|
||||
const val PUBLICATION_COMPLETE = 61
|
||||
const val CANCELLED = 62
|
||||
const val HIATUS = 63
|
||||
// SY <--
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
|
||||
interface BrowseSourceFilterHeader {
|
||||
fun getFilterHeader(controller: Controller): RecyclerView.Adapter<*>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import rx.Observable
|
||||
|
||||
interface FollowsSource {
|
||||
fun fetchFollows(): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns a list of all Follows retrieved by Coroutines
|
||||
*
|
||||
* @param SManga all smanga found for user
|
||||
*/
|
||||
fun fetchAllFollows(forceHd: Boolean = false): Flow<List<Pair<SManga, RaisedSearchMetadata>>>
|
||||
|
||||
/**
|
||||
* updates the follow status for a manga
|
||||
*/
|
||||
fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Get a MdList Track of the manga
|
||||
*/
|
||||
fun fetchTrackingInfo(url: String): Flow<Track>
|
||||
}
|
||||
@@ -13,11 +13,9 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.log.maybeInjectEHLogger
|
||||
import exh.patch.injectPatches
|
||||
import exh.source.DelegatedHttpSource
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -25,6 +23,9 @@ import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website.
|
||||
@@ -36,22 +37,24 @@ abstract class HttpSource : CatalogueSource {
|
||||
*/
|
||||
// SY -->
|
||||
protected val network: NetworkHelper by lazy {
|
||||
val original = Injekt.get<NetworkHelper>()
|
||||
val network = Injekt.get<NetworkHelper>()
|
||||
object : NetworkHelper(Injekt.get<Application>()) {
|
||||
override val client: OkHttpClient
|
||||
get() = delegate?.networkHttpClient ?: original.client
|
||||
get() = delegate?.networkHttpClient ?: network.client
|
||||
.newBuilder()
|
||||
.injectPatches { id }
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
override val cloudflareClient: OkHttpClient
|
||||
get() = delegate?.networkCloudflareClient ?: original.cloudflareClient
|
||||
get() = delegate?.networkCloudflareClient ?: network.cloudflareClient
|
||||
.newBuilder()
|
||||
.injectPatches { id }
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
override val cookieManager: AndroidCookieJar
|
||||
get() = original.cookieManager
|
||||
get() = network.cookieManager
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
@@ -88,7 +91,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Headers used for requests.
|
||||
*/
|
||||
val headers: Headers by lazy { headersBuilder().build() }
|
||||
/* SY --> */ open /* SY <-- */ val headers: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.app.Activity
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
interface LoginSource {
|
||||
val needsLogin: Boolean
|
||||
|
||||
fun isLogged(): Boolean
|
||||
|
||||
fun getLoginDialog(source: Source, activity: Activity): DialogController
|
||||
|
||||
suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean
|
||||
|
||||
suspend fun logout(): Boolean
|
||||
}
|
||||
@@ -13,16 +13,16 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.source.EnhancedHttpSource
|
||||
import kotlin.reflect.KClass
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* LEWD!
|
||||
*/
|
||||
interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
val db: DatabaseHelper get() = Injekt.get()
|
||||
|
||||
/**
|
||||
@@ -55,8 +55,7 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
Single.fromCallable {
|
||||
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
||||
}.map {
|
||||
if (it != null) it.raise(metaClass)
|
||||
else newMetaInstance()
|
||||
it?.raise(metaClass) ?: newMetaInstance()
|
||||
}
|
||||
} else {
|
||||
Single.just(newMetaInstance())
|
||||
@@ -112,17 +111,14 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
||||
|
||||
companion object {
|
||||
fun Source.isLewdSource() = (this is LewdSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>))
|
||||
fun Source.isMetadataSource() = (this is MetadataSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is MetadataSource<*, *>))
|
||||
|
||||
fun Source.getLewdSource(): LewdSource<*, *>? {
|
||||
return if (!this.isLewdSource()) {
|
||||
null
|
||||
} else if (this is LewdSource<*, *>) {
|
||||
this
|
||||
} else if (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>) {
|
||||
this.enhancedSource
|
||||
} else {
|
||||
null
|
||||
fun Source.getMetadataSource(): MetadataSource<*, *>? {
|
||||
return when {
|
||||
!this.isMetadataSource() -> null
|
||||
this is MetadataSource<*, *> -> this
|
||||
this is EnhancedHttpSource && this.enhancedSource is MetadataSource<*, *> -> this.enhancedSource
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface RandomMangaSource {
|
||||
fun fetchRandomMangaUrl(): Flow<String>
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.util.asObservable
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website, but for Coroutines.
|
||||
*/
|
||||
abstract class SuspendHttpSource : HttpSource() {
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return fetchPopularMangaFlow(page).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchPopularMangaFlow(page: Int): Flow<MangasPage> {
|
||||
return flow {
|
||||
val response = client.newCall(popularMangaRequestSuspended(page)).await()
|
||||
emit(
|
||||
popularMangaParseSuspended(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
final override fun popularMangaRequest(page: Int): Request {
|
||||
return runBlocking { popularMangaRequestSuspended(page) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun popularMangaRequestSuspended(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun popularMangaParse(response: Response): MangasPage {
|
||||
return runBlocking { popularMangaParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun popularMangaParseSuspended(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return fetchSearchMangaSuspended(page, query, filters).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): Flow<MangasPage> {
|
||||
return flow {
|
||||
val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await()
|
||||
emit(
|
||||
searchMangaParseSuspended(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
final override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return runBlocking { searchMangaRequestSuspended(page, query, filters) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun searchMangaRequestSuspended(page: Int, query: String, filters: FilterList): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun searchMangaParse(response: Response): MangasPage {
|
||||
return runBlocking { searchMangaParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun searchMangaParseSuspended(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return fetchLatestUpdatesFlow(page).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchLatestUpdatesFlow(page: Int): Flow<MangasPage> {
|
||||
return flow {
|
||||
val response = client.newCall(latestUpdatesRequestSuspended(page)).await()
|
||||
emit(
|
||||
latestUpdatesParseSuspended(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
final override fun latestUpdatesRequest(page: Int): Request {
|
||||
return runBlocking { latestUpdatesRequestSuspended(page) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun latestUpdatesRequestSuspended(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
return runBlocking { latestUpdatesParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun latestUpdatesParseSuspended(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return fetchMangaDetailsFlow(manga).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
|
||||
return flow {
|
||||
val response = client.newCall(mangaDetailsRequestSuspended(manga)).await()
|
||||
emit(
|
||||
mangaDetailsParseSuspended(response).apply { initialized = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
final override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return runBlocking { mangaDetailsRequestSuspended(manga) }
|
||||
}
|
||||
|
||||
open suspend fun mangaDetailsRequestSuspended(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun mangaDetailsParse(response: Response): SManga {
|
||||
return runBlocking { mangaDetailsParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun mangaDetailsParseSuspended(response: Response): SManga
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return try {
|
||||
fetchChapterListFlow(manga).asObservable()
|
||||
} catch (e: LicencedException) {
|
||||
Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LicencedException::class)
|
||||
open fun fetchChapterListFlow(manga: SManga): Flow<List<SChapter>> {
|
||||
return flow {
|
||||
if (manga.status != SManga.LICENSED) {
|
||||
val response = client.newCall(chapterListRequestSuspended(manga)).await()
|
||||
emit(
|
||||
chapterListParseSuspended(response)
|
||||
)
|
||||
} else {
|
||||
throw LicencedException("Licensed - No chapters to show")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
final override fun chapterListRequest(manga: SManga): Request {
|
||||
return runBlocking { chapterListRequestSuspended(manga) }
|
||||
}
|
||||
|
||||
protected open suspend fun chapterListRequestSuspended(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return runBlocking { chapterListParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun chapterListParseSuspended(response: Response): List<SChapter>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return fetchPageListFlow(chapter).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchPageListFlow(chapter: SChapter): Flow<List<Page>> {
|
||||
return flow {
|
||||
val response = client.newCall(pageListRequestSuspended(chapter)).await()
|
||||
emit(
|
||||
pageListParseSuspended(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
final override fun pageListRequest(chapter: SChapter): Request {
|
||||
return runBlocking { pageListRequestSuspended(chapter) }
|
||||
}
|
||||
|
||||
protected open suspend fun pageListRequestSuspended(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun pageListParse(response: Response): List<Page> {
|
||||
return runBlocking { pageListParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun pageListParseSuspended(response: Response): List<Page>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return fetchImageUrlFlow(page).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchImageUrlFlow(page: Page): Flow<String> {
|
||||
return flow {
|
||||
val response = client.newCall(imageUrlRequestSuspended(page)).await()
|
||||
emit(
|
||||
imageUrlParseSuspended(response)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||
* override the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
final override fun imageUrlRequest(page: Page): Request {
|
||||
return runBlocking { imageUrlRequestSuspended(page) }
|
||||
}
|
||||
|
||||
protected open suspend fun imageUrlRequestSuspended(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
final override fun imageUrlParse(response: Response): String {
|
||||
return runBlocking { imageUrlParseSuspended(response) }
|
||||
}
|
||||
|
||||
protected abstract suspend fun imageUrlParseSuspended(response: Response): String
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
final override fun fetchImage(page: Page): Observable<Response> {
|
||||
return fetchImageFlow(page).asObservable()
|
||||
}
|
||||
|
||||
open fun fetchImageFlow(page: Page): Flow<Response> {
|
||||
return flow {
|
||||
emit(
|
||||
client.newCallWithProgress(imageRequestSuspended(page), page).await()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
final override fun imageRequest(page: Page): Request {
|
||||
return runBlocking { imageRequestSuspended(page) }
|
||||
}
|
||||
|
||||
protected open suspend fun imageRequestSuspended(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||
*
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
final override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
runBlocking { prepareNewChapterSuspended(chapter, manga) }
|
||||
}
|
||||
|
||||
open suspend fun prepareNewChapterSuspended(chapter: SChapter, manga: SManga) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = runBlocking { getFilterListSuspended() }
|
||||
|
||||
open suspend fun getFilterListSuspended() = FilterList()
|
||||
|
||||
companion object {
|
||||
data class LicencedException(override val message: String?) : Exception()
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,20 @@ import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.annoations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -47,12 +49,12 @@ import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.asObservableWithAsyncStacktrace
|
||||
import exh.util.dropBlank
|
||||
import exh.util.ignore
|
||||
import exh.util.nullIfBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.trimOrNull
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.net.URLEncoder
|
||||
import java.util.ArrayList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.CookieJar
|
||||
@@ -68,16 +70,19 @@ import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.util.ArrayList
|
||||
|
||||
// TODO Consider gallery updating when doing tabbed browsing
|
||||
@Nsfw
|
||||
class EHentai(
|
||||
override val id: Long,
|
||||
val exh: Boolean,
|
||||
val context: Context
|
||||
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
||||
) : HttpSource(), MetadataSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
||||
override val metaClass = EHentaiSearchMetadata::class
|
||||
|
||||
val domain: String
|
||||
private val domain: String
|
||||
get() = if (exh) {
|
||||
"exhentai.org"
|
||||
} else {
|
||||
@@ -88,9 +93,9 @@ class EHentai(
|
||||
get() = "https://$domain"
|
||||
|
||||
override val lang = "all"
|
||||
override val supportsLatest = true
|
||||
override val supportsLatest = !exh
|
||||
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val updateHelper: EHentaiUpdateHelper by injectLazy()
|
||||
|
||||
/**
|
||||
@@ -103,11 +108,11 @@ class EHentai(
|
||||
val parsedMangas = select(".itg > tbody > tr").filter {
|
||||
// Do not parse header and ads
|
||||
it.selectFirst("th") == null && it.selectFirst(".itd") == null
|
||||
}.map {
|
||||
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||
val column2 = it.selectFirst(".gl3e, .gl2c")
|
||||
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||
val infoElement = it.selectFirst(".gl3e")
|
||||
}.map { body ->
|
||||
val thumbnailElement = body.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||
val column2 = body.selectFirst(".gl3e, .gl2c")
|
||||
val linkElement = body.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||
val infoElement = body.selectFirst(".gl3e")
|
||||
|
||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||
val infoElements = infoElement?.select("div")
|
||||
@@ -142,7 +147,7 @@ class EHentai(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val tagElement = it.selectFirst(".gl3c > a")
|
||||
val tagElement = body.selectFirst(".gl3c > a")
|
||||
val tagElements = tagElement.select("div")
|
||||
tagElements.forEach { element ->
|
||||
if (element.className() == "gt") {
|
||||
@@ -172,11 +177,11 @@ class EHentai(
|
||||
|
||||
getPageCount(infoElements.getOrNull(5))?.let { length = it }
|
||||
} else {
|
||||
val parsedGenre = it.selectFirst(".gl1c div")
|
||||
val parsedGenre = body.selectFirst(".gl1c div")
|
||||
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
|
||||
|
||||
val info = it.selectFirst(".gl2c")
|
||||
val extraInfo = it.selectFirst(".gl4c")
|
||||
val info = body.selectFirst(".gl2c")
|
||||
val extraInfo = body.selectFirst(".gl4c")
|
||||
|
||||
val infoList = info.select("div div")
|
||||
|
||||
@@ -392,7 +397,7 @@ class EHentai(
|
||||
}
|
||||
|
||||
// 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) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
@@ -441,14 +446,14 @@ class EHentai(
|
||||
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
||||
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
|
||||
|
||||
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(
|
||||
page?.let {
|
||||
addParam(url, "page", Integer.toString(page - 1))
|
||||
} ?: url,
|
||||
additionalHeaders?.let {
|
||||
additionalHeaders?.let { additionalHeadersNotNull ->
|
||||
val headers = headers.newBuilder()
|
||||
it.toMultimap().forEach { (t, u) ->
|
||||
additionalHeadersNotNull.toMultimap().forEach { (t, u) ->
|
||||
u.forEach {
|
||||
headers.add(t, it)
|
||||
}
|
||||
@@ -597,12 +602,10 @@ class EHentai(
|
||||
RaisedTag(
|
||||
namespace,
|
||||
element.text().trim(),
|
||||
if (element.hasClass("gtl")) {
|
||||
TAG_TYPE_LIGHT
|
||||
} else if (element.hasClass("gtw")) {
|
||||
TAG_TYPE_WEAK
|
||||
} else {
|
||||
TAG_TYPE_NORMAL
|
||||
when {
|
||||
element.hasClass("gtl") -> TAG_TYPE_LIGHT
|
||||
element.hasClass("gtw") -> TAG_TYPE_WEAK
|
||||
else -> TAG_TYPE_NORMAL
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -627,7 +630,7 @@ class EHentai(
|
||||
.map { realImageUrlParse(it, page) }
|
||||
}
|
||||
|
||||
fun realImageUrlParse(response: Response, page: Page): String {
|
||||
private fun realImageUrlParse(response: Response, page: Page): String {
|
||||
with(response.asJsoup()) {
|
||||
val currentImage = getElementById("img").attr("src")
|
||||
// Each press of the retry button will choose another server
|
||||
@@ -678,30 +681,30 @@ class EHentai(
|
||||
}
|
||||
|
||||
fun spPref() = if (exh) {
|
||||
prefs.eh_exhSettingsProfile()
|
||||
preferences.eh_exhSettingsProfile()
|
||||
} else {
|
||||
prefs.eh_ehSettingsProfile()
|
||||
preferences.eh_ehSettingsProfile()
|
||||
}
|
||||
|
||||
fun rawCookies(sp: Int): Map<String, String> {
|
||||
private fun rawCookies(sp: Int): Map<String, String> {
|
||||
val cookies: MutableMap<String, String> = mutableMapOf()
|
||||
if (prefs.enableExhentai().get()) {
|
||||
cookies[LoginController.MEMBER_ID_COOKIE] = prefs.memberIdVal().get()
|
||||
cookies[LoginController.PASS_HASH_COOKIE] = prefs.passHashVal().get()
|
||||
cookies[LoginController.IGNEOUS_COOKIE] = prefs.igneousVal().get()
|
||||
if (preferences.enableExhentai().get()) {
|
||||
cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
|
||||
cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get()
|
||||
cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get()
|
||||
cookies["sp"] = sp.toString()
|
||||
|
||||
val sessionKey = prefs.eh_settingsKey().get()
|
||||
val sessionKey = preferences.eh_settingsKey().get()
|
||||
if (sessionKey.isNotBlank()) {
|
||||
cookies["sk"] = sessionKey
|
||||
}
|
||||
|
||||
val sessionCookie = prefs.eh_sessionCookie().get()
|
||||
val sessionCookie = preferences.eh_sessionCookie().get()
|
||||
if (sessionCookie.isNotBlank()) {
|
||||
cookies["s"] = sessionCookie
|
||||
}
|
||||
|
||||
val hathPerksCookie = prefs.eh_hathPerksCookies().get()
|
||||
val hathPerksCookie = preferences.eh_hathPerksCookies().get()
|
||||
if (hathPerksCookie.isNotBlank()) {
|
||||
cookies["hath_perks"] = hathPerksCookie
|
||||
}
|
||||
@@ -721,7 +724,7 @@ class EHentai(
|
||||
// Headers
|
||||
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())
|
||||
|
||||
fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
||||
private fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
||||
.buildUpon()
|
||||
.appendQueryParameter(param, value)
|
||||
.toString()
|
||||
@@ -746,9 +749,10 @@ class EHentai(
|
||||
return FilterList(
|
||||
AutoCompleteTags(
|
||||
EHTags.getNameSpaces().map { "$it:" } + EHTags.getAllTags(),
|
||||
EHTags.getNameSpaces().map { "$it:" }, excludePrefix
|
||||
EHTags.getNameSpaces().map { "$it:" },
|
||||
excludePrefix
|
||||
),
|
||||
if (prefs.eh_watchedListDefaultState().get()) {
|
||||
if (preferences.eh_watchedListDefaultState().get()) {
|
||||
Watched(isEnabled = true)
|
||||
} else {
|
||||
Watched(isEnabled = false)
|
||||
@@ -816,14 +820,13 @@ class EHentai(
|
||||
private fun combineQuery(filters: FilterList): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter ->
|
||||
val splitState = filter.state.map(String::trim).filterNot(String::isBlank)
|
||||
val splitState = filter.state.trimAll().dropBlank()
|
||||
splitState.mapNotNull { tag ->
|
||||
val split = tag.split(":").filterNot { it.isBlank() }.toMutableList()
|
||||
val split = tag.split(":").filterNot { it.isBlank() }
|
||||
if (split.size > 1) {
|
||||
val namespace = split[0].removePrefix("-")
|
||||
val exclude = split[0].startsWith("-")
|
||||
split -= namespace
|
||||
AdvSearchEntry(Pair(namespace, split.joinToString(":")), exclude)
|
||||
AdvSearchEntry(Pair(namespace, split[1]), exclude)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -865,7 +868,7 @@ class EHentai(
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state > 0) {
|
||||
builder.appendQueryParameter("f_srdd", Integer.toString(state + 1))
|
||||
builder.appendQueryParameter("f_srdd", (state + 1).toString())
|
||||
builder.appendQueryParameter("f_sr", "on")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -18,19 +18,17 @@ import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HitomiSearchMetadata, Document>,
|
||||
MetadataSource<HitomiSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = HitomiSearchMetadata::class
|
||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||
override val id: Long
|
||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||
override val lang = if (id == otherId) "all" else delegate.lang
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
@@ -87,7 +85,8 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
characters = content.select("a").map { it.text() }
|
||||
tags += characters.map {
|
||||
RaisedTag(
|
||||
"character", it,
|
||||
"character",
|
||||
it,
|
||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
@@ -98,7 +97,8 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
else if (it.attr("href").startsWith("/tag/female")) "female"
|
||||
else "misc"
|
||||
RaisedTag(
|
||||
ns, it.text().dropLast(if (ns == "misc") 0 else 2),
|
||||
ns,
|
||||
it.text().dropLast(if (ns == "misc") 0 else 2),
|
||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
@@ -114,7 +114,13 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
|
||||
override fun ensureDelegateCompatible() {
|
||||
if (versionId != delegate.versionId) {
|
||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId})!")
|
||||
}
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"hitomi.la"
|
||||
|
||||
@@ -1,40 +1,268 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.BrowseSourceFilterHeader
|
||||
import eu.kanade.tachiyomi.source.online.FollowsSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.GalleryAddEvent
|
||||
import exh.GalleryAdder
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.md.handlers.ApiChapterParser
|
||||
import exh.md.handlers.ApiMangaParser
|
||||
import exh.md.handlers.FollowsHandler
|
||||
import exh.md.handlers.MangaHandler
|
||||
import exh.md.handlers.MangaPlusHandler
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdLang
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import exh.widget.preference.MangadexLoginDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
ConfigurableSource,
|
||||
UrlImportableSource {
|
||||
MetadataSource<MangaDexSearchMetadata, Response>,
|
||||
UrlImportableSource,
|
||||
FollowsSource,
|
||||
LoginSource,
|
||||
BrowseSourceFilterHeader,
|
||||
RandomMangaSource {
|
||||
override val lang: String = delegate.lang
|
||||
|
||||
override val headers: Headers
|
||||
get() = super.headers.newBuilder().apply {
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
add("Referer", MdUtil.baseUrl)
|
||||
}.build()
|
||||
|
||||
private val mdLang by lazy {
|
||||
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
||||
}
|
||||
|
||||
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
importIdToMdId(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||
|
||||
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
|
||||
"/manga/${uri.pathSegments[1]}"
|
||||
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override val lang: String get() = delegate.lang
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return MangaHandler(client, headers, listOf(mdLang), preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = (delegate as ConfigurableSource).setupPreferenceScreen(screen)
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return MangaHandler(client, headers, listOf(mdLang), preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
|
||||
}
|
||||
|
||||
@ExperimentalSerializationApi
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return if (chapter.scanlator == "MangaPlus") {
|
||||
client.newCall(mangaPlusPageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val chapterId = ApiChapterParser().externalParse(response)
|
||||
MangaPlusHandler(client).fetchPageList(chapterId)
|
||||
}
|
||||
} else super.fetchPageList(chapter)
|
||||
}
|
||||
|
||||
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
|
||||
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix)
|
||||
return GET(MdUtil.baseUrl + chpUrl + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun fetchImage(page: Page): Observable<Response> {
|
||||
return if (page.imageUrl!!.contains("mangaplus", true)) {
|
||||
MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
|
||||
.asObservableSuccess()
|
||||
} else super.fetchImage(page)
|
||||
}
|
||||
|
||||
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): MangaDexDescriptionAdapter {
|
||||
return MangaDexDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
||||
ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input, preferences.mangaDexForceLatestCovers().get())
|
||||
}
|
||||
|
||||
override fun fetchFollows(): Observable<MangasPage> {
|
||||
return FollowsHandler(client, headers, Injekt.get()).fetchFollows()
|
||||
}
|
||||
|
||||
override val needsLogin: Boolean = true
|
||||
|
||||
override fun getLoginDialog(source: Source, activity: Activity): DialogController {
|
||||
return MangadexLoginDialog(source as MangaDex)
|
||||
}
|
||||
|
||||
override fun isLogged(): Boolean {
|
||||
val httpUrl = MdUtil.baseUrl.toHttpUrlOrNull()!!
|
||||
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||
}
|
||||
|
||||
override suspend fun login(
|
||||
username: String,
|
||||
password: String,
|
||||
twoFactorCode: String
|
||||
): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("login_username", username)
|
||||
.add("login_password", password)
|
||||
.add("no_js", "1")
|
||||
.add("remember_me", "1")
|
||||
|
||||
twoFactorCode.let {
|
||||
formBody.add("two_factor", it)
|
||||
}
|
||||
|
||||
val response = client.newCall(
|
||||
POST(
|
||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||
headers,
|
||||
formBody.build()
|
||||
)
|
||||
).execute()
|
||||
response.body!!.string().isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun logout(): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
// https://mangadex.org/ajax/actions.ajax.php?function=logout
|
||||
val httpUrl = MdUtil.baseUrl.toHttpUrlOrNull()!!
|
||||
val listOfDexCookies = network.cookieManager.get(httpUrl)
|
||||
val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
||||
val token = cookie?.value
|
||||
if (token.isNullOrEmpty()) {
|
||||
return@withContext true
|
||||
}
|
||||
val result = client.newCall(
|
||||
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
||||
).execute()
|
||||
val resultStr = result.body!!.string()
|
||||
if (resultStr.contains("success", true)) {
|
||||
network.cookieManager.remove(httpUrl)
|
||||
trackManager.mdList.logout()
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchAllFollows(forceHd: Boolean): Flow<List<Pair<SManga, MangaDexSearchMetadata>>> {
|
||||
return flow { emit(FollowsHandler(client, headers, Injekt.get()).fetchAllFollows(forceHd)) }
|
||||
}
|
||||
|
||||
fun updateReadingProgress(track: Track): Flow<Boolean> {
|
||||
return flow { FollowsHandler(client, headers, Injekt.get()).updateReadingProgress(track) }
|
||||
}
|
||||
|
||||
fun updateRating(track: Track): Flow<Boolean> {
|
||||
return flow { FollowsHandler(client, headers, Injekt.get()).updateRating(track) }
|
||||
}
|
||||
|
||||
override fun fetchTrackingInfo(url: String): Flow<Track> {
|
||||
return flow {
|
||||
if (!isLogged()) {
|
||||
throw Exception("Not Logged in")
|
||||
}
|
||||
emit(FollowsHandler(client, headers, Injekt.get()).fetchTrackingInfo(url))
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean> {
|
||||
return flow { emit(FollowsHandler(client, headers, Injekt.get()).updateFollowStatus(mangaID, followStatus)) }
|
||||
}
|
||||
|
||||
override fun getFilterHeader(controller: Controller): MangaDexFabHeaderAdapter {
|
||||
return MangaDexFabHeaderAdapter(controller, this)
|
||||
}
|
||||
|
||||
override fun fetchRandomMangaUrl(): Flow<String> {
|
||||
return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId()
|
||||
}
|
||||
|
||||
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
||||
when {
|
||||
query.toIntOrNull() != null -> {
|
||||
Observable.fromCallable {
|
||||
// MdUtil.
|
||||
val res = GalleryAdder().addGallery(context, MdUtil.baseUrl + MdUtil.mapMdIdToMangaUrl(query.toInt()), false, this)
|
||||
MangasPage(
|
||||
(
|
||||
if (res is GalleryAddEvent.Success) {
|
||||
listOf(res.manga)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> fail()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REMEMBER_ME = "mangadex_rememberme_token"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,203 +1,189 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.util.Log
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.SuspendHttpSource
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.util.asFlow
|
||||
import exh.util.await
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.rx2.asFlowable
|
||||
import kotlinx.coroutines.rx2.asSingle
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.single
|
||||
import kotlinx.coroutines.flow.singleOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
// TODO LocalSource compatibility
|
||||
// TODO Disable clear database option
|
||||
class MergedSource : HttpSource() {
|
||||
class MergedSource : SuspendHttpSource() {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val gson: Gson by injectLazy()
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
override val id: Long = MERGED_SOURCE_ID
|
||||
|
||||
override val baseUrl = ""
|
||||
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun popularMangaRequestSuspended(page: Int) = throw UnsupportedOperationException()
|
||||
override suspend fun popularMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun searchMangaRequestSuspended(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
|
||||
override suspend fun latestUpdatesParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||
override fun fetchChapterListFlow(manga: SManga) = throw UnsupportedOperationException()
|
||||
override fun fetchImageFlow(page: Page) = throw UnsupportedOperationException()
|
||||
override fun fetchImageUrlFlow(page: Page) = throw UnsupportedOperationException()
|
||||
override fun fetchPageListFlow(chapter: SChapter) = throw UnsupportedOperationException()
|
||||
override fun fetchLatestUpdatesFlow(page: Int) = throw UnsupportedOperationException()
|
||||
override fun fetchPopularMangaFlow(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return RxJavaInterop.toV1Observable(
|
||||
readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded ->
|
||||
override fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
|
||||
return flow {
|
||||
val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
|
||||
val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
|
||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
|
||||
if (mangaReferences.size == 1 || {
|
||||
val mangaReference = mangaReferences.firstOrNull()
|
||||
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
|
||||
}()
|
||||
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
|
||||
|
||||
emit(
|
||||
SManga.create().apply {
|
||||
this.copyFrom(loaded.manga)
|
||||
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
||||
val dbManga = mangaInfoReference?.let { withContext(Dispatchers.IO) { db.getManga(it.mangaUrl, it.mangaSourceId).await() } }
|
||||
this.copyFrom(dbManga ?: mergedManga)
|
||||
url = manga.url
|
||||
}
|
||||
}.asFlowable()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return RxJavaInterop.toV1Single(
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer()
|
||||
loadedMangas.map { loadedManga ->
|
||||
async(Dispatchers.IO) {
|
||||
loadedManga.source.fetchChapterList(loadedManga.manga).map { chapterList ->
|
||||
chapterList.map { chapter ->
|
||||
chapter.apply {
|
||||
url = writeUrlConfig(UrlConfig(loadedManga.source.id, url, loadedManga.manga.url))
|
||||
}
|
||||
fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
|
||||
// TODO more chapter dedupe
|
||||
return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable()
|
||||
.asFlow()
|
||||
.map { chapterList ->
|
||||
val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() }
|
||||
val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId }
|
||||
if (editScanlators) {
|
||||
chapterList.onEach { chapter ->
|
||||
val source = sources.firstOrNull { chapter.manga_id == it.second }?.first
|
||||
if (source != null) {
|
||||
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
|
||||
else "$source: ${chapter.scanlator}"
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dedupe) dedupeChapterList(mangaReferences, chapterList) else chapterList
|
||||
}
|
||||
}
|
||||
|
||||
private fun dedupeChapterList(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
|
||||
return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) {
|
||||
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList
|
||||
MergedMangaReference.CHAPTER_SORT_PRIORITY -> chapterList
|
||||
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> {
|
||||
findSourceWithMostChapters(chapterList)?.let { mangaId ->
|
||||
chapterList.filter { it.manga_id == mangaId }
|
||||
} ?: chapterList
|
||||
}
|
||||
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> {
|
||||
findSourceWithHighestChapterNumber(chapterList)?.let { mangaId ->
|
||||
chapterList.filter { it.manga_id == mangaId }
|
||||
} ?: chapterList
|
||||
}
|
||||
else -> chapterList
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSourceWithMostChapters(chapterList: List<Chapter>): Long? {
|
||||
return chapterList.groupBy { it.manga_id }.maxByOrNull { it.value.size }?.key
|
||||
}
|
||||
|
||||
private fun findSourceWithHighestChapterNumber(chapterList: List<Chapter>): Long? {
|
||||
return chapterList.maxByOrNull { it.chapter_number }?.manga_id
|
||||
}
|
||||
|
||||
fun fetchChaptersForMergedManga(manga: Manga, downloadChapters: Boolean = true, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
|
||||
return flow {
|
||||
withContext(Dispatchers.IO) {
|
||||
fetchChaptersAndSync(manga, downloadChapters).collect()
|
||||
}
|
||||
emit(
|
||||
getChaptersFromDB(manga, editScanlators, dedupe).singleOrNull() ?: emptyList<Chapter>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Flow<Pair<List<Chapter>, List<Chapter>>> {
|
||||
val mangaReferences = db.getMergedMangaReferences(manga.id!!).await()
|
||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
||||
|
||||
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
||||
return mangaReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.asFlow().map {
|
||||
load(db, sourceManager, it)
|
||||
}.buffer().flatMapMerge { loadedManga ->
|
||||
withContext(Dispatchers.IO) {
|
||||
if (loadedManga.manga != null && loadedManga.reference.getChapterUpdates) {
|
||||
loadedManga.source.fetchChapterList(loadedManga.manga).asFlow()
|
||||
.map { syncChaptersWithSource(db, it, loadedManga.manga, loadedManga.source) }
|
||||
.onEach {
|
||||
if (ifDownloadNewChapters && loadedManga.reference.downloadChapters) {
|
||||
downloadManager.downloadChapters(loadedManga.manga, it.first)
|
||||
}
|
||||
}.toSingle().await(Schedulers.io())
|
||||
}
|
||||
}.buffer().map { it.await() }.toList().flatten()
|
||||
}.asSingle(Dispatchers.IO)
|
||||
).toObservable()
|
||||
}
|
||||
} else {
|
||||
emptyList<Pair<List<Chapter>, List<Chapter>>>().asFlow()
|
||||
}
|
||||
}
|
||||
}.buffer()
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
val config = readUrlConfig(chapter.url)
|
||||
val source = sourceManager.getOrStub(config.source)
|
||||
return source.fetchPageList(
|
||||
SChapter.create().apply {
|
||||
copyFrom(chapter)
|
||||
url = config.url
|
||||
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource {
|
||||
var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await()
|
||||
val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId)
|
||||
if (manga == null) {
|
||||
manga = Manga.create(reference.mangaSourceId).apply {
|
||||
url = reference.mangaUrl
|
||||
}
|
||||
).map { pages ->
|
||||
pages.map { page ->
|
||||
page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl)))
|
||||
manga.copyFrom(source.fetchMangaDetails(manga).asFlow().single())
|
||||
try {
|
||||
manga.id = db.insertManga(manga).await().insertedId()
|
||||
reference.mangaId = manga.id
|
||||
db.insertNewMergedMangaId(reference).await()
|
||||
} catch (e: Exception) {
|
||||
XLog.st(e.stackTrace.contentToString(), 5)
|
||||
}
|
||||
}
|
||||
return LoadedMangaSource(source, manga, reference)
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
val config = readUrlConfig(page.url)
|
||||
val source = sourceManager.getOrStub(config.source) as? HttpSource
|
||||
?: throw UnsupportedOperationException("This source does not support this operation!")
|
||||
return source.fetchImageUrl(page.copyWithUrl(config.url))
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchImage(page: Page): Observable<Response> {
|
||||
val config = readUrlConfig(page.url)
|
||||
val source = sourceManager.getOrStub(config.source) as? HttpSource
|
||||
?: throw UnsupportedOperationException("This source does not support this operation!")
|
||||
return source.fetchImage(page.copyWithUrl(config.url))
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
val chapterConfig = readUrlConfig(chapter.url)
|
||||
val source = sourceManager.getOrStub(chapterConfig.source) as? HttpSource
|
||||
?: throw UnsupportedOperationException("This source does not support this operation!")
|
||||
val copiedManga = SManga.create().apply {
|
||||
this.copyFrom(manga)
|
||||
url = chapterConfig.mangaUrl
|
||||
}
|
||||
chapter.url = chapterConfig.url
|
||||
source.prepareNewChapter(chapter, copiedManga)
|
||||
chapter.url = writeUrlConfig(UrlConfig(source.id, chapter.url, chapterConfig.mangaUrl))
|
||||
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
|
||||
else "$source: ${chapter.scanlator}"
|
||||
}
|
||||
|
||||
fun readMangaConfig(manga: SManga): MangaConfig {
|
||||
return MangaConfig.readFromUrl(gson, manga.url)
|
||||
}
|
||||
|
||||
fun readUrlConfig(url: String): UrlConfig {
|
||||
return gson.fromJson(url)
|
||||
}
|
||||
|
||||
fun writeUrlConfig(urlConfig: UrlConfig): String {
|
||||
return gson.toJson(urlConfig)
|
||||
}
|
||||
|
||||
data class LoadedMangaSource(val source: Source, val manga: Manga)
|
||||
data class MangaSource(
|
||||
@SerializedName("s")
|
||||
val source: Long,
|
||||
@SerializedName("u")
|
||||
val url: String
|
||||
) {
|
||||
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? {
|
||||
val manga = db.getManga(url, source).executeAsBlocking() ?: return null
|
||||
val source = sourceManager.getOrStub(source)
|
||||
return LoadedMangaSource(source, manga)
|
||||
}
|
||||
}
|
||||
|
||||
data class MangaConfig(
|
||||
@SerializedName("c")
|
||||
val children: List<MangaSource>
|
||||
) {
|
||||
fun load(db: DatabaseHelper, sourceManager: SourceManager): Flow<LoadedMangaSource> {
|
||||
return children.asFlow().map { mangaSource ->
|
||||
mangaSource.load(db, sourceManager)
|
||||
?: run {
|
||||
XLog.w("> Missing source manga: $mangaSource")
|
||||
Log.d("MERGED", "> Missing source manga: $mangaSource")
|
||||
throw IllegalStateException("Missing source manga: $mangaSource")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writeAsUrl(gson: Gson): String {
|
||||
return gson.toJson(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun readFromUrl(gson: Gson, url: String): MangaConfig {
|
||||
return gson.fromJson(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UrlConfig(
|
||||
@SerializedName("s")
|
||||
val source: Long,
|
||||
@SerializedName("u")
|
||||
val url: String,
|
||||
@SerializedName("m")
|
||||
val mangaUrl: String
|
||||
)
|
||||
|
||||
fun Page.copyWithUrl(newUrl: String) = Page(
|
||||
index,
|
||||
newUrl,
|
||||
imageUrl,
|
||||
uri
|
||||
)
|
||||
data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference)
|
||||
|
||||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
@@ -13,7 +13,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
@@ -25,14 +25,12 @@ import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
open class NHentai(delegate: HttpSource, val context: Context) :
|
||||
class NHentai(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<NHentaiSearchMetadata, Response>,
|
||||
MetadataSource<NHentaiSearchMetadata, Response>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = NHentaiSearchMetadata::class
|
||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||
override val id: Long
|
||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||
override val lang = if (id == otherId) "all" else delegate.lang
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
@@ -100,7 +98,13 @@ open class NHentai(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
|
||||
override fun ensureDelegateCompatible() {
|
||||
if (versionId != delegate.versionId) {
|
||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId})!")
|
||||
}
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"nhentai.net"
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -24,7 +24,7 @@ import rx.Observable
|
||||
|
||||
class PervEden(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PervEdenSearchMetadata, Document>,
|
||||
MetadataSource<PervEdenSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = PervEdenSearchMetadata::class
|
||||
override val lang = delegate.lang
|
||||
@@ -77,7 +77,8 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
artist = it.text()
|
||||
tags += RaisedTag(
|
||||
"artist", it.text().toLowerCase(),
|
||||
"artist",
|
||||
it.text().toLowerCase(),
|
||||
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
|
||||
)
|
||||
}
|
||||
@@ -85,7 +86,8 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
||||
"Genres" -> {
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
tags += RaisedTag(
|
||||
null, it.text().toLowerCase(),
|
||||
null,
|
||||
it.text().toLowerCase(),
|
||||
PervEdenSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -22,7 +22,7 @@ import rx.Observable
|
||||
|
||||
class EightMuses(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<EightMusesSearchMetadata, Document>,
|
||||
MetadataSource<EightMusesSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = EightMusesSearchMetadata::class
|
||||
override val lang = "en"
|
||||
@@ -65,8 +65,8 @@ class EightMuses(delegate: HttpSource, val context: Context) :
|
||||
thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
|
||||
?.selectFirst(".lazyload")
|
||||
?.attr("data-src")?.let {
|
||||
baseUrl + it
|
||||
}
|
||||
baseUrl + it
|
||||
}
|
||||
|
||||
tags.clear()
|
||||
tags += RaisedTag(
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -22,7 +22,7 @@ import rx.Observable
|
||||
|
||||
class HBrowse(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HBrowseSearchMetadata, Document>,
|
||||
MetadataSource<HBrowseSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = HBrowseSearchMetadata::class
|
||||
override val lang = "en"
|
||||
|
||||
@@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -24,7 +24,7 @@ import rx.Observable
|
||||
|
||||
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HentaiCafeSearchMetadata, Document>,
|
||||
MetadataSource<HentaiCafeSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -24,7 +24,7 @@ import rx.Observable
|
||||
|
||||
class Pururin(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PururinSearchMetadata, Document>,
|
||||
MetadataSource<PururinSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@@ -20,14 +20,14 @@ import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Tsumino(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<TsuminoSearchMetadata, Document>,
|
||||
MetadataSource<TsuminoSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = TsuminoSearchMetadata::class
|
||||
override val lang = "en"
|
||||
|
||||
@@ -7,11 +7,11 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
|
||||
@@ -49,6 +49,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
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
|
||||
else -> R.style.Theme_Tachiyomi_Dark
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.ui.base.changehandler
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
|
||||
|
||||
/**
|
||||
* An [AnimatorChangeHandler] that will cross fade two views
|
||||
*/
|
||||
class OneWayFadeChangeHandler : AnimatorChangeHandler {
|
||||
constructor()
|
||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||
constructor(duration: Long) : super(duration)
|
||||
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(
|
||||
duration,
|
||||
removesFromViewOnPush
|
||||
)
|
||||
|
||||
override fun getAnimator(container: ViewGroup, from: View?, to: View?, isPush: Boolean, toAddedToContainer: Boolean): Animator {
|
||||
val animator = AnimatorSet()
|
||||
if (to != null) {
|
||||
val start: Float = if (toAddedToContainer) 0F else to.alpha
|
||||
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
|
||||
}
|
||||
|
||||
if (from != null && (!isPush || removesFromViewOnPush())) {
|
||||
container.removeView(from)
|
||||
}
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun resetFromView(from: View) {
|
||||
from.alpha = 1f
|
||||
}
|
||||
|
||||
override fun copy(): ControllerChangeHandler {
|
||||
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
|
||||
}
|
||||
}
|
||||
@@ -22,27 +22,29 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
lateinit var binding: VB
|
||||
|
||||
init {
|
||||
addLifecycleListener(object : LifecycleListener() {
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
onViewCreated(view)
|
||||
}
|
||||
addLifecycleListener(
|
||||
object : LifecycleListener() {
|
||||
override fun postCreateView(controller: Controller, view: View) {
|
||||
onViewCreated(view)
|
||||
}
|
||||
|
||||
override fun preCreateView(controller: Controller) {
|
||||
Timber.d("Create view for ${controller.instance()}")
|
||||
}
|
||||
override fun preCreateView(controller: Controller) {
|
||||
Timber.d("Create view for ${controller.instance()}")
|
||||
}
|
||||
|
||||
override fun preAttach(controller: Controller, view: View) {
|
||||
Timber.d("Attach view for ${controller.instance()}")
|
||||
}
|
||||
override fun preAttach(controller: Controller, view: View) {
|
||||
Timber.d("Attach view for ${controller.instance()}")
|
||||
}
|
||||
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
Timber.d("Detach view for ${controller.instance()}")
|
||||
}
|
||||
override fun preDetach(controller: Controller, view: View) {
|
||||
Timber.d("Detach view for ${controller.instance()}")
|
||||
}
|
||||
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
Timber.d("Destroy view for ${controller.instance()}")
|
||||
override fun preDestroyView(controller: Controller, view: View) {
|
||||
Timber.d("Destroy view for ${controller.instance()}")
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
override val containerView: View?
|
||||
@@ -98,17 +100,19 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
var expandActionViewFromInteraction = false
|
||||
|
||||
fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) {
|
||||
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return onExpand?.invoke(item) ?: true
|
||||
}
|
||||
setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return onExpand?.invoke(item) ?: true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
activity?.invalidateOptionsMenu()
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
activity?.invalidateOptionsMenu()
|
||||
|
||||
return onCollapse?.invoke(item) ?: true
|
||||
return onCollapse?.invoke(item) ?: true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (expandActionViewFromInteraction) {
|
||||
expandActionViewFromInteraction = false
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
|
||||
|
||||
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||
val controller = getControllerWithTag(tag)
|
||||
@@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
|
||||
}
|
||||
}
|
||||
|
||||
fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction {
|
||||
fun Controller.withFadeTransaction(): RouterTransaction {
|
||||
return RouterTransaction.with(this)
|
||||
.pushChangeHandler(FadeChangeHandler(duration))
|
||||
.popChangeHandler(FadeChangeHandler(duration))
|
||||
.pushChangeHandler(OneWayFadeChangeHandler())
|
||||
.popChangeHandler(OneWayFadeChangeHandler())
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private typealias ExtensionTuple =
|
||||
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
||||
|
||||