Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 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.0)
|
||||||
- I have updated all extensions
|
- 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
|
- 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 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.0)
|
||||||
- I have updated all extensions
|
- 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
|
- 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 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.0)
|
||||||
- I have updated all extensions
|
- 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
|
- 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
|
name: Issue closer
|
||||||
on: [issues]
|
on: [issues]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -31,4 +32,4 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
type: body
|
type: body
|
||||||
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
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
|
name: Pull request build check
|
||||||
|
on: [pull_request]
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
apk:
|
build:
|
||||||
name: Generate APK
|
runs-on: ubuntu-latest
|
||||||
runs-on: ubuntu-18.04
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Clone repo
|
||||||
- name: set up JDK 1.8
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 1.8
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 1.8
|
java-version: 1.8
|
||||||
- name: Get NDK
|
- name: Install NDK
|
||||||
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
||||||
- name: Build Release APK
|
- name: Build project
|
||||||
run: bash ./gradlew assembleDebug --stacktrace
|
run: ./gradlew assembleDebug
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: TachiyomiSY-${{ github.sha }}.apk
|
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'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||||
// Realm (EH)
|
// Realm (EH)
|
||||||
apply plugin: 'realm-android'
|
apply plugin: 'realm-android'
|
||||||
@@ -42,8 +43,8 @@ android {
|
|||||||
minSdkVersion AndroidConfig.minSdk
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion AndroidConfig.targetSdk
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 6
|
versionCode 8
|
||||||
versionName "1.2.0"
|
versionName "1.3.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@@ -65,7 +66,6 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
ext.enableCrashlytics = false
|
|
||||||
}
|
}
|
||||||
releaseTest {
|
releaseTest {
|
||||||
applicationIdSuffix ".rt"
|
applicationIdSuffix ".rt"
|
||||||
@@ -140,11 +140,11 @@ dependencies {
|
|||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
implementation 'androidx.biometric:biometric:1.1.0-alpha02'
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.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.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||||
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
@@ -152,20 +152,20 @@ dependencies {
|
|||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
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-common-java8:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
// Job scheduling
|
// 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:$work_version"
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
// UI library
|
// UI library
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
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
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
@@ -174,14 +174,14 @@ dependencies {
|
|||||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// 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:okhttp:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$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
|
// TLS 1.3 support for Android < 10
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
implementation 'org.conscrypt:conscrypt-android:2.5.1'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.9.0'
|
final retrofit_version = '2.9.0'
|
||||||
@@ -217,7 +217,7 @@ dependencies {
|
|||||||
implementation 'io.requery:sqlite-android:3.32.2'
|
implementation 'io.requery:sqlite-android:3.32.2'
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.1'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
@@ -277,13 +277,12 @@ dependencies {
|
|||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
final aboutlibraries_version = '8.3.0'
|
// NOTE: REMEMBER TO UPDATE GRADLE PLUGIN
|
||||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
implementation 'com.mikepenz:aboutlibraries:8.3.0'
|
||||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation 'junit:junit:4.13'
|
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'
|
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||||
|
|
||||||
final robolectric_version = '3.1.4'
|
final robolectric_version = '3.1.4'
|
||||||
@@ -291,54 +290,38 @@ dependencies {
|
|||||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||||
testImplementation "org.robolectric:shadows-play-services:$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:$BuildPluginsVersion.KOTLIN"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
|
||||||
|
// 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-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
// 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)
|
// Text distance (EH)
|
||||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
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)
|
// 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)
|
// Better logging (EH)
|
||||||
implementation 'com.elvishew:xlog:1.6.1'
|
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)
|
// Debug utils (EH)
|
||||||
debugImplementation 'com.ms-square:debugoverlay:1.1.3'
|
final def debug_overlay_version = '1.1.3'
|
||||||
releaseTestImplementation 'com.ms-square:debugoverlay:1.1.3'
|
debugImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||||
releaseImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
releaseTestImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||||
testImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
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'
|
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
||||||
|
|
||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
@@ -346,7 +329,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
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:core:$markwon_version"
|
||||||
implementation "io.noties.markwon:ext-strikethrough:$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:image:$markwon_version"
|
||||||
implementation "io.noties.markwon:linkify:$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 {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.72'
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
tasks.withType(AbstractKotlinCompile).all {
|
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
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
@@ -384,10 +366,10 @@ task copyResources(type: Copy) {
|
|||||||
include '**/*'
|
include '**/*'
|
||||||
}
|
}
|
||||||
|
|
||||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
preBuild.dependsOn(formatKotlin, copyResources)
|
||||||
|
|
||||||
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
// Firebase (EH)
|
// Firebase Crashlytics
|
||||||
apply plugin: 'io.fabric'
|
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" />
|
android:scheme="https" />
|
||||||
|
|
||||||
<!-- MangaDex -->
|
<!-- MangaDex -->
|
||||||
<!-- <data
|
<data
|
||||||
android:host="mangadex.org"
|
android:host="mangadex.org"
|
||||||
android:pathPattern="\/(title|manga)\/"
|
android:pathPattern="\/(title|manga)\/"
|
||||||
android:scheme="http" />
|
android:scheme="http" />
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<data
|
<data
|
||||||
android:host="www.mangadex.org"
|
android:host="www.mangadex.org"
|
||||||
android:pathPattern="\/(title|manga)\/"
|
android:pathPattern="\/(title|manga)\/"
|
||||||
android:scheme="https" />-->
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<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.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
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.DebugOverlay
|
||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
@@ -34,13 +36,9 @@ import exh.debug.DebugToggles
|
|||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
|
import exh.syDebugVersion
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
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.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
@@ -49,13 +47,23 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.InjektScope
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
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 {
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
|
if (!BuildConfig.DEBUG) addAnalytics()
|
||||||
|
|
||||||
workaroundAndroid7BrokenSSL()
|
workaroundAndroid7BrokenSSL()
|
||||||
|
|
||||||
@@ -79,7 +87,6 @@ open class App : Application(), LifecycleObserver {
|
|||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
Realm.init(this)
|
Realm.init(this)
|
||||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
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) {
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
setupDebugOverlay()
|
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)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun onAppBackgrounded() {
|
fun onAppBackgrounded() {
|
||||||
@@ -159,15 +173,14 @@ open class App : Application(), LifecycleObserver {
|
|||||||
private fun setupExhLogging() {
|
private fun setupExhLogging() {
|
||||||
EHLogLevel.init(this)
|
EHLogLevel.init(this)
|
||||||
|
|
||||||
val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
|
val logLevel = when {
|
||||||
LogLevel.ALL
|
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
||||||
} else {
|
BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||||
LogLevel.WARN
|
else -> LogLevel.WARN
|
||||||
}
|
}
|
||||||
|
|
||||||
val logConfig = LogConfiguration.Builder()
|
val logConfig = LogConfiguration.Builder()
|
||||||
.logLevel(logLevel)
|
.logLevel(logLevel)
|
||||||
.t()
|
|
||||||
.st(2)
|
.st(2)
|
||||||
.nb()
|
.nb()
|
||||||
.build()
|
.build()
|
||||||
@@ -180,14 +193,17 @@ open class App : Application(), LifecycleObserver {
|
|||||||
"logs"
|
"logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
printers += FilePrinter
|
printers += FilePrinter
|
||||||
.Builder(logFolder.absolutePath)
|
.Builder(logFolder.absolutePath)
|
||||||
.fileNameGenerator(object : DateFileNameGenerator() {
|
.fileNameGenerator(
|
||||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
object : DateFileNameGenerator() {
|
||||||
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
|
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())
|
.backupStrategy(NeverBackupStrategy())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -202,6 +218,17 @@ open class App : Application(), LifecycleObserver {
|
|||||||
)
|
)
|
||||||
|
|
||||||
XLog.d("Application booting...")
|
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
|
// EXH
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
@@ -36,8 +36,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(), TimeUnit.HOURS,
|
interval.toLong(),
|
||||||
10, TimeUnit.MINUTES
|
TimeUnit.HOURS,
|
||||||
|
10,
|
||||||
|
TimeUnit.MINUTES
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -32,6 +32,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.EXTENSIONS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
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.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.SAVEDSEARCHES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
@@ -39,6 +40,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
|||||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
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.backup.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
@@ -57,17 +59,23 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import exh.EXHSavedSearch
|
import exh.EXHSavedSearch
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
import java.lang.RuntimeException
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import kotlin.math.max
|
import exh.util.asObservable
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
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) {
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
|
|
||||||
@@ -106,6 +114,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
|
// SY -->
|
||||||
|
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||||
|
// SY <--
|
||||||
.create()
|
.create()
|
||||||
else -> throw Exception("Json version unknown")
|
else -> throw Exception("Json version unknown")
|
||||||
}
|
}
|
||||||
@@ -129,15 +140,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Create extension ID/name mapping
|
// Create extension ID/name mapping
|
||||||
val extensionEntries = JsonArray()
|
val extensionEntries = JsonArray()
|
||||||
|
|
||||||
|
// Merged Manga References
|
||||||
|
val mergedMangaReferenceEntries = JsonArray()
|
||||||
|
|
||||||
// Add value's to root
|
// Add value's to root
|
||||||
root[Backup.VERSION] = CURRENT_VERSION
|
root[Backup.VERSION] = CURRENT_VERSION
|
||||||
root[Backup.MANGAS] = mangaEntries
|
root[Backup.MANGAS] = mangaEntries
|
||||||
root[CATEGORIES] = categoryEntries
|
root[CATEGORIES] = categoryEntries
|
||||||
root[EXTENSIONS] = extensionEntries
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
// SY -->
|
||||||
|
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
|
||||||
|
// SY <--
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
// Get manga from database
|
// Get manga from database
|
||||||
val mangas = getFavoriteManga()
|
val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
@@ -163,6 +180,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// SY -->
|
// SY -->
|
||||||
root[SAVEDSEARCHES] =
|
root[SAVEDSEARCHES] =
|
||||||
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
||||||
|
|
||||||
|
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +231,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
|
* Backup the categories of library
|
||||||
*
|
*
|
||||||
@@ -317,29 +343,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>>> {
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
// SY -->
|
// SY -->
|
||||||
return (
|
if (source is MergedSource) {
|
||||||
if (source is EHentai) {
|
val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
|
||||||
source.fetchChapterList(manga, throttleManager::throttle)
|
return syncedChapters.onEach { pair ->
|
||||||
} 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()) {
|
if (pair.first.isNotEmpty()) {
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
insertChapters(chapters)
|
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 +621,49 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
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
|
||||||
|
val mergedManga = (if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga) ?: return@forEach
|
||||||
|
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 <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -602,6 +682,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
internal fun getFavoriteManga(): List<Manga> =
|
internal fun getFavoriteManga(): List<Manga> =
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
|
internal fun getMergedManga(): List<Manga> =
|
||||||
|
databaseHelper.getMergedMangas().executeAsBlocking()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts manga and returns id
|
* 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.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
internal class BackupNotifier(private val context: Context) {
|
internal class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,11 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
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.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
import exh.eh.EHentaiThrottleManager
|
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.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -48,6 +45,10 @@ import kotlinx.coroutines.launch
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
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.
|
* Restores backup from a JSON file.
|
||||||
@@ -238,7 +239,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalAmount = mangasJson.size()
|
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()
|
skippedAmount = mangasJson.size() - validManga.count()
|
||||||
// SY <--
|
// SY <--
|
||||||
restoreProgress = 0
|
restoreProgress = 0
|
||||||
@@ -288,6 +289,15 @@ class BackupRestoreService : Service() {
|
|||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
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.categories))
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
@@ -445,7 +455,12 @@ class BackupRestoreService : Service() {
|
|||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.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())
|
Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ object Backup {
|
|||||||
const val VERSION = "version"
|
const val VERSION = "version"
|
||||||
// SY -->
|
// SY -->
|
||||||
const val SAVEDSEARCHES = "savedsearches"
|
const val SAVEDSEARCHES = "savedsearches"
|
||||||
|
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
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.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -22,6 +20,8 @@ import okio.buffer
|
|||||||
import okio.sink
|
import okio.sink
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create chapter cache
|
* 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.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
|
import exh.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.SearchMetadataTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
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.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* EXH --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@@ -51,11 +54,12 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
// EXH -->
|
// SY -->
|
||||||
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
// EXH <--
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
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.HistoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MergedTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
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.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
import exh.metadata.sql.tables.SearchTitleTable
|
import exh.metadata.sql.tables.SearchTitleTable
|
||||||
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* 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) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -34,14 +34,12 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
execSQL(HistoryTable.createTableQuery)
|
execSQL(HistoryTable.createTableQuery)
|
||||||
// EXH -->
|
// SY -->
|
||||||
execSQL(SearchMetadataTable.createTableQuery)
|
execSQL(SearchMetadataTable.createTableQuery)
|
||||||
execSQL(SearchTagTable.createTableQuery)
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
// EXH <--
|
|
||||||
// AZ -->
|
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
// AZ <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
@@ -49,17 +47,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
// EXH -->
|
// SY -->
|
||||||
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||||
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||||
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||||
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||||
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
// EXH <--
|
|
||||||
// AZ -->
|
|
||||||
execSQL(MergedTable.createIndexQuery)
|
execSQL(MergedTable.createIndexQuery)
|
||||||
// AZ <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
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.addDateAdded)
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(MergedTable.dropTableQuery)
|
||||||
|
db.execSQL(MergedTable.createTableQuery)
|
||||||
|
db.execSQL(MergedTable.createIndexQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ class LibraryManga : MangaImpl() {
|
|||||||
var unread: Int = 0
|
var unread: Int = 0
|
||||||
|
|
||||||
var category: Int = 0
|
var category: Int = 0
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var read: Int = 0
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import java.util.Date
|
|||||||
|
|
||||||
interface ChapterQueries : DbProvider {
|
interface ChapterQueries : DbProvider {
|
||||||
// SY -->
|
// 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)
|
.listOfObjects(Chapter::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
@@ -27,15 +27,6 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getChaptersByMergedMangaId(mangaId: Long) = db.get()
|
|
||||||
.listOfObjects(Chapter::class.java)
|
|
||||||
.withQuery(
|
|
||||||
RawQuery.builder()
|
|
||||||
.query(getMergedChaptersQuery(mangaId))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun getRecentChapters(date: Date) = db.get()
|
fun getRecentChapters(date: Date) = db.get()
|
||||||
@@ -94,6 +85,17 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.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 <--
|
// SY <--
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
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.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
@@ -77,15 +78,6 @@ interface MangaQueries : DbProvider {
|
|||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
// SY -->
|
// 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()
|
fun updateMangaInfo(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaInfoPutResolver())
|
.withPutResolver(MangaInfoPutResolver())
|
||||||
@@ -139,7 +131,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.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)
|
.whereArgs(0)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
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.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
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.HistoryTable as History
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
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.MangaTable as Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MergedTable as Merged
|
import exh.merged.sql.tables.MergedTable as Merged
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
/**
|
/**
|
||||||
* Query to get the manga merged into a merged manga
|
* Query to get the manga merged into a merged manga
|
||||||
*/
|
*/
|
||||||
fun getMergedMangaQuery(id: Long) =
|
fun getMergedMangaQuery() =
|
||||||
"""
|
"""
|
||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
FROM (
|
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
|
) AS M
|
||||||
JOIN ${Manga.TABLE}
|
JOIN ${Manga.TABLE}
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
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
|
* Query to get the chapters of all manga in a merged manga
|
||||||
*/
|
*/
|
||||||
fun getMergedChaptersQuery(id: Long) =
|
fun getMergedChaptersQuery() =
|
||||||
"""
|
"""
|
||||||
SELECT ${Chapter.TABLE}.*
|
SELECT ${Chapter.TABLE}.*
|
||||||
FROM (
|
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
|
) AS M
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
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.
|
* 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}
|
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 0
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
) AS C
|
) AS C
|
||||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1
|
LEFT JOIN (
|
||||||
GROUP BY ${Manga.COL_ID}
|
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}
|
ORDER BY ${Manga.COL_TITLE}
|
||||||
) AS M
|
) AS M
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
SELECT * FROM ${MangaCategory.TABLE}
|
||||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
) 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.
|
* 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)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
||||||
|
// SY -->
|
||||||
|
manga.read = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_READ))
|
||||||
|
// SY <--
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_UNREAD = "unread"
|
const val COL_UNREAD = "unread"
|
||||||
|
|
||||||
|
// SY ->>
|
||||||
|
const val COL_READ = "read"
|
||||||
|
// SY <--
|
||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
|
|
||||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
||||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
|||||||
if (sourceDir != null) {
|
if (sourceDir != null) {
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||||
if (mangaDir != null) {
|
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
|
return false
|
||||||
@@ -145,7 +145,7 @@ class DownloadCache(
|
|||||||
mangaDirs.values.forEach { mangaDir ->
|
mangaDirs.values.forEach { mangaDir ->
|
||||||
val chapterDirs = mangaDir.dir.listFiles()
|
val chapterDirs = mangaDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.mapNotNull { it.name }
|
.mapNotNull { it.name?.replace(".cbz", "") }
|
||||||
.toHashSet()
|
.toHashSet()
|
||||||
|
|
||||||
mangaDir.files = chapterDirs
|
mangaDir.files = chapterDirs
|
||||||
@@ -196,6 +196,8 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
|
} else if ("$it.cbz" in mangaDir.files) {
|
||||||
|
mangaDir.files -= "$it.cbz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,6 +228,8 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
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 manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||||
queue.remove(chapters)
|
val filteredChapters = getChaptersToDelete(chapters)
|
||||||
|
|
||||||
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
queue.remove(filteredChapters)
|
||||||
chapters.filterNot { it.bookmark }
|
|
||||||
} else {
|
|
||||||
chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||||
chapterDirs.forEach { it.delete() }
|
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
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filteredChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// 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
|
* 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 manga the manga of the chapters.
|
||||||
* @param source the source 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
|
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)
|
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||||
cleaned += filesWithNoChapter.size
|
cleaned += filesWithNoChapter.size
|
||||||
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||||
filesWithNoChapter.forEach { it.delete() }
|
filesWithNoChapter.forEach { it.delete() }
|
||||||
val readChapters = allChapters.filter { it.read }
|
|
||||||
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
if (removeRead) {
|
||||||
readChapterDirs.forEach { it.delete() }
|
val readChapters = allChapters.filter { it.read }
|
||||||
cleaned += readChapterDirs.size
|
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||||
cache.removeChapters(readChapters, manga)
|
readChapterDirs.forEach { it.delete() }
|
||||||
|
cleaned += readChapterDirs.size
|
||||||
|
cache.removeChapters(readChapters, manga)
|
||||||
|
}
|
||||||
|
|
||||||
if (cache.getDownloadCount(manga) == 0) {
|
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
|
return cleaned
|
||||||
}
|
}
|
||||||
@@ -260,7 +285,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
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
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldFolder = oldNames.asSequence()
|
val oldFolder = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
||||||
if (oldFolder?.renameTo(newName) == true) {
|
if (oldFolder?.renameTo(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "") == true) {
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName, mangaDir, manga)
|
cache.addChapter(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "", mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
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.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* 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(
|
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()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(manga, source)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return getValidChapterDirNames(chapter).asSequence()
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it) }
|
.mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return chapters.mapNotNull { chapter ->
|
return chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter).asSequence()
|
getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,10 +127,10 @@ class DownloadProvider(private val context: Context) {
|
|||||||
(
|
(
|
||||||
chapters.find { chp ->
|
chapters.find { chp ->
|
||||||
getValidChapterDirNames(chp).any { dir ->
|
getValidChapterDirNames(chp).any { dir ->
|
||||||
mangaDir.findFile(dir) != null
|
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
||||||
}
|
}
|
||||||
} == null
|
} == null
|
||||||
) || it.name?.endsWith("_tmp") == true
|
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// 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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
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.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
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.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@@ -30,7 +30,13 @@ import rx.android.schedulers.AndroidSchedulers
|
|||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
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.
|
* This class is the one in charge of downloading chapters.
|
||||||
@@ -53,6 +59,8 @@ class Downloader(
|
|||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val chapterCache: ChapterCache by injectLazy()
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -464,7 +472,39 @@ class Downloader(
|
|||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
// Only rename the directory if it's downloaded.
|
||||||
if (download.status == Download.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)
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class DownloadQueue(
|
class DownloadQueue(
|
||||||
private val store: DownloadStore,
|
private val store: DownloadStore,
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import android.util.Log
|
|||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
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.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import java.io.InputStream
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
* 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.module.AppGlideModule
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import java.io.InputStream
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to update Glide module settings
|
* Class used to update Glide module settings
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
val json = try {
|
val json = try {
|
||||||
Gson().fromJson(
|
Gson().fromJson(
|
||||||
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java
|
Scanner(editJson).useDelimiter("\\Z").next(),
|
||||||
|
JsonObject::class.java
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
@@ -83,7 +84,12 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
fun Manga.toJson(): MangaJson {
|
fun Manga.toJson(): MangaJson {
|
||||||
return 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.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
@@ -45,8 +45,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
interval.toLong(), TimeUnit.HOURS,
|
interval.toLong(),
|
||||||
10, TimeUnit.MINUTES
|
TimeUnit.HOURS,
|
||||||
|
10,
|
||||||
|
TimeUnit.MINUTES
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.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.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class LibraryUpdateNotifier(private val context: Context) {
|
class LibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total 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()) {
|
val title = if (preferences.hideNotificationContent()) {
|
||||||
context.getString(R.string.notification_check_updates)
|
context.getString(R.string.notification_check_updates)
|
||||||
} else {
|
} else {
|
||||||
@@ -198,18 +199,23 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
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(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
manga,
|
||||||
|
chapters,
|
||||||
|
Notifications.ID_NEW_CHAPTERS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// View chapters action
|
// View chapters action
|
||||||
addAction(
|
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(
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
context,
|
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.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
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.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
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.LibraryUpdateRanker.rankingScheme
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.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.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
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.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||||
import java.io.File
|
import exh.MERGED_SOURCE_ID
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
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.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.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
|
* 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 {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
COVERS, // Manga covers
|
COVERS, // Manga covers
|
||||||
TRACKING // Tracking metadata
|
TRACKING, // Tracking metadata
|
||||||
|
// SY -->
|
||||||
|
SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -87,6 +108,14 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
const val KEY_TARGET = "target"
|
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.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
@@ -106,11 +135,15 @@ class LibraryUpdateService(
|
|||||||
* @param target defines what should be updated.
|
* @param target defines what should be updated.
|
||||||
* @return true if service newly started, false otherwise
|
* @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)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(KEY_TARGET, target)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
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) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@@ -194,6 +227,9 @@ class LibraryUpdateService(
|
|||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
Target.COVERS -> updateCovers(mangaList)
|
Target.COVERS -> updateCovers(mangaList)
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
|
// SY -->
|
||||||
|
Target.SYNC_FOLLOWS -> syncFollows()
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -221,10 +257,15 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
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) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
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)
|
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty()) {
|
if (categoriesToUpdate.isNotEmpty()) {
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
@@ -233,6 +274,43 @@ class LibraryUpdateService(
|
|||||||
} else {
|
} else {
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
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()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
@@ -275,7 +353,12 @@ class LibraryUpdateService(
|
|||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.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())
|
Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
@@ -328,7 +411,12 @@ class LibraryUpdateService(
|
|||||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
// We don't want to start downloading while the library is updating, because websites
|
// 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.
|
// 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.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
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
|
// Update manga details metadata in the background
|
||||||
if (preferences.autoUpdateMetadata()) {
|
if (preferences.autoUpdateMetadata()) {
|
||||||
@@ -360,8 +448,38 @@ class LibraryUpdateService(
|
|||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.fetchChapterList(manga)
|
// SY -->
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
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> {
|
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.
|
* Writes basic file of update errors to cache dir.
|
||||||
*/
|
*/
|
||||||
@@ -437,7 +597,8 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
destFile.bufferedWriter().use { out ->
|
||||||
errors.forEach { (manga, error) ->
|
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
|
return destFile
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
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.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.io.File
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global [BroadcastReceiver] that runs on UI thread
|
* Global [BroadcastReceiver] that runs on UI thread
|
||||||
@@ -56,19 +56,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Launch share activity and dismiss notification
|
// Launch share activity and dismiss notification
|
||||||
ACTION_SHARE_IMAGE ->
|
ACTION_SHARE_IMAGE ->
|
||||||
shareImage(
|
shareImage(
|
||||||
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
context,
|
||||||
|
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Delete image from path and dismiss notification
|
// Delete image from path and dismiss notification
|
||||||
ACTION_DELETE_IMAGE ->
|
ACTION_DELETE_IMAGE ->
|
||||||
deleteImage(
|
deleteImage(
|
||||||
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
context,
|
||||||
|
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Share backup file
|
// Share backup file
|
||||||
ACTION_SHARE_BACKUP ->
|
ACTION_SHARE_BACKUP ->
|
||||||
shareBackup(
|
shareBackup(
|
||||||
context, intent.getParcelableExtra(EXTRA_URI),
|
context,
|
||||||
|
intent.getParcelableExtra(EXTRA_URI),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||||
@@ -80,7 +83,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(
|
openChapter(
|
||||||
context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
context,
|
||||||
|
intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,53 +82,62 @@ object Notifications {
|
|||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
CHANNEL_COMMON,
|
||||||
|
context.getString(R.string.channel_common),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
CHANNEL_LIBRARY,
|
||||||
|
context.getString(R.string.channel_library),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
CHANNEL_DOWNLOADER_PROGRESS,
|
||||||
|
context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_DOWNLOADER
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
CHANNEL_DOWNLOADER_COMPLETE,
|
||||||
|
context.getString(R.string.channel_complete),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_DOWNLOADER
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors),
|
CHANNEL_DOWNLOADER_ERROR,
|
||||||
|
context.getString(R.string.channel_errors),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_DOWNLOADER
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
CHANNEL_NEW_CHAPTERS,
|
||||||
|
context.getString(R.string.channel_new_chapters),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
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
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
|
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
||||||
|
context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACKUP_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
|
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
||||||
|
context.getString(R.string.channel_complete),
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACKUP_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val filterCompleted = "pref_filter_completed_key"
|
const val filterCompleted = "pref_filter_completed_key"
|
||||||
|
|
||||||
|
const val filterStarted = "pref_filter_started_key"
|
||||||
|
|
||||||
const val filterTracked = "pref_filter_tracked_key"
|
const val filterTracked = "pref_filter_tracked_key"
|
||||||
|
|
||||||
const val filterLewd = "pref_filter_lewd_key"
|
const val filterLewd = "pref_filter_lewd_key"
|
||||||
@@ -280,4 +282,38 @@ object PreferenceKeys {
|
|||||||
const val startReadingButton = "start_reading_button"
|
const val startReadingButton = "start_reading_button"
|
||||||
|
|
||||||
const val groupLibraryBy = "group_library_by"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ object PreferenceValues {
|
|||||||
default,
|
default,
|
||||||
blue,
|
blue,
|
||||||
amoled,
|
amoled,
|
||||||
|
red,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class DisplayMode {
|
||||||
COMPACT_GRID,
|
COMPACT_GRID,
|
||||||
COMFORTABLE_GRID,
|
COMFORTABLE_GRID,
|
||||||
|
// SY -->
|
||||||
|
NO_TITLE_GRID,
|
||||||
|
// SY <--
|
||||||
LIST,
|
LIST,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,4 +47,12 @@ object PreferenceValues {
|
|||||||
PARTIAL,
|
PARTIAL,
|
||||||
BLOCKED
|
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.FlowSharedPreferences
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
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.DisplayMode
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import kotlinx.coroutines.flow.Flow
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
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 filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0)
|
||||||
|
|
||||||
|
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, 0)
|
||||||
|
|
||||||
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0)
|
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0)
|
||||||
|
|
||||||
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0)
|
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0)
|
||||||
@@ -385,4 +387,38 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
||||||
|
|
||||||
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
|
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, false)
|
||||||
|
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
|
||||||
@@ -15,8 +16,24 @@ class TrackManager(context: Context) {
|
|||||||
const val KITSU = 3
|
const val KITSU = 3
|
||||||
const val SHIKIMORI = 4
|
const val SHIKIMORI = 4
|
||||||
const val BANGUMI = 5
|
const val BANGUMI = 5
|
||||||
|
|
||||||
|
// 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 myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
|
||||||
val aniList = Anilist(context, ANILIST)
|
val aniList = Anilist(context, ANILIST)
|
||||||
@@ -27,9 +44,25 @@ class TrackManager(context: Context) {
|
|||||||
|
|
||||||
val bangumi = Bangumi(context, BANGUMI)
|
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 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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import java.util.Calendar
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
@@ -271,9 +271,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ALManga(
|
return ALManga(
|
||||||
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
struct["id"].asInt,
|
||||||
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(),
|
struct["title"]["romaji"].asString,
|
||||||
date, struct["chapters"].nullInt ?: 0
|
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.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val media_id: Int,
|
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.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import java.net.URLEncoder
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import java.text.DecimalFormat
|
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
|||||||
@@ -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.lang.toCalendar
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
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.FormBody
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -30,6 +23,13 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import rx.Observable
|
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) {
|
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 androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
@@ -23,7 +24,7 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
try {
|
try {
|
||||||
val result = UpdateChecker.getUpdateChecker().checkForUpdate()
|
val result = GithubUpdateChecker().checkForUpdate()
|
||||||
|
|
||||||
if (result is UpdateResult.NewUpdate<*>) {
|
if (result is UpdateResult.NewUpdate<*>) {
|
||||||
val url = result.release.downloadLink
|
val url = result.release.downloadLink
|
||||||
@@ -65,8 +66,10 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
||||||
3, TimeUnit.DAYS,
|
3,
|
||||||
3, TimeUnit.HOURS
|
TimeUnit.DAYS,
|
||||||
|
3,
|
||||||
|
TimeUnit.HOURS
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.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.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import java.io.File
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class UpdaterService : Service() {
|
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.
|
* Assets class containing download url.
|
||||||
* @param downloadLink 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.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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 {
|
interface GithubService {
|
||||||
|
|
||||||
@@ -24,11 +25,6 @@ interface GithubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
@GET("/repos/{repo}/releases/latest")
|
||||||
@GET("/repos/jobobby04/tachiyomiSY/releases/latest")
|
suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease
|
||||||
suspend fun getLatestVersion(): GithubRelease
|
|
||||||
|
|
||||||
@GET("/repos/jobobby04/TachiyomiSYPreview/releases/latest")
|
|
||||||
suspend fun getLatestDebugVersion(): GithubRelease
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,49 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
package eu.kanade.tachiyomi.data.updater.github
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
// SY -->
|
|
||||||
class GithubUpdateChecker(val debug: Boolean = false) : UpdateChecker() {
|
class GithubUpdateChecker {
|
||||||
|
|
||||||
private val service: GithubService = GithubService.create()
|
private val service: GithubService = GithubService.create()
|
||||||
|
|
||||||
override suspend fun checkForUpdate(): UpdateResult {
|
private val repo: String by lazy {
|
||||||
val release = if (syDebugVersion != "0") {
|
// Sy -->
|
||||||
service.getLatestDebugVersion()
|
if (syDebugVersion != "0") {
|
||||||
|
"jobobby04/TachiyomiSYPreview"
|
||||||
} else {
|
} 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
|
// 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)) {
|
return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) {
|
||||||
|
// SY <--
|
||||||
GithubUpdateResult.NewUpdate(release)
|
GithubUpdateResult.NewUpdate(release)
|
||||||
} else {
|
} else {
|
||||||
GithubUpdateResult.NoNewUpdate()
|
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.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
@@ -73,8 +73,10 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
|
||||||
12, TimeUnit.HOURS,
|
12,
|
||||||
1, TimeUnit.HOURS
|
TimeUnit.HOURS,
|
||||||
|
1,
|
||||||
|
TimeUnit.HOURS
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.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.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import java.util.Date
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
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.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
|
|||||||
@@ -178,7 +178,13 @@ internal object ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val extension = Extension.Installed(
|
val extension = Extension.Installed(
|
||||||
extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
|
extName,
|
||||||
|
pkgName,
|
||||||
|
versionName,
|
||||||
|
versionCode,
|
||||||
|
lang,
|
||||||
|
isNsfw,
|
||||||
|
sources,
|
||||||
isUnofficial = signatureHash != officialSignature
|
isUnofficial = signatureHash != officialSignature
|
||||||
)
|
)
|
||||||
return LoadResult.Success(extension)
|
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.isOutdated
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
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.Cookie
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
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 {
|
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
@@ -89,7 +89,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
var isWebViewOutdated = false
|
var isWebViewOutdated = false
|
||||||
|
|
||||||
val origRequestUrl = request.url.toString()
|
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 {
|
handler.post {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package eu.kanade.tachiyomi.network
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.io.File
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import uy.kohesive.injekt.injectLazy
|
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) {
|
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
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 kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
@@ -13,6 +9,10 @@ import okhttp3.Response
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
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> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
@@ -54,22 +54,24 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||||
return suspendCancellableCoroutine { continuation ->
|
return suspendCancellableCoroutine { continuation ->
|
||||||
enqueue(object : Callback {
|
enqueue(
|
||||||
override fun onResponse(call: Call, response: Response) {
|
object : Callback {
|
||||||
if (assertSuccess && !response.isSuccessful) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
if (assertSuccess && !response.isSuccessful) {
|
||||||
return
|
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 {
|
continuation.invokeOnCancellation {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
@@ -8,6 +7,7 @@ import okio.BufferedSource
|
|||||||
import okio.ForwardingSource
|
import okio.ForwardingSource
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||||
private val DEFAULT_HEADERS = Headers.Builder().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.GsonBuilder
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.R
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@@ -15,6 +16,12 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
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.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -22,10 +29,6 @@ import java.util.Locale
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
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 {
|
class LocalSource(private val context: Context) : CatalogueSource {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -74,6 +77,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val baseDirs = getBaseDirectories(context)
|
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
|
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
var mangaDirs = baseDirs
|
var mangaDirs = baseDirs
|
||||||
@@ -81,6 +87,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
.mapNotNull { it.listFiles()?.toList() }
|
.mapNotNull { it.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory }
|
.filter { it.isDirectory }
|
||||||
|
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
|
||||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||||
.distinctBy { it.name }
|
.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.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
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.MergedSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||||
@@ -31,13 +32,13 @@ import exh.TSUMINO_SOURCE_ID
|
|||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
@@ -191,14 +192,14 @@ open class SourceManager(private val context: Context) {
|
|||||||
TSUMINO_SOURCE_ID,
|
TSUMINO_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||||
Tsumino::class
|
Tsumino::class
|
||||||
)/*,
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"MangaDex",
|
"MangaDex",
|
||||||
fillInSourceId,
|
fillInSourceId,
|
||||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||||
MangaDex::class,
|
MangaDex::class,
|
||||||
true
|
true
|
||||||
)*/,
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"HBrowse",
|
"HBrowse",
|
||||||
HBROWSE_SOURCE_ID,
|
HBROWSE_SOURCE_ID,
|
||||||
|
|||||||
@@ -2,16 +2,26 @@ package eu.kanade.tachiyomi.source.model
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
|
import exh.util.DataSaver
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
|
|
||||||
open class Page(
|
open class Page(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
/* SY --> */
|
/* SY --> */
|
||||||
var /* SY <-- */ url: String = "",
|
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
|
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||||
) : ProgressListener {
|
) : ProgressListener {
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var imageUrl = imageUrl
|
||||||
|
get() {
|
||||||
|
if (field == null) return null
|
||||||
|
return DataSaver().compress(field!!)
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
val number: Int
|
val number: Int
|
||||||
get() = index + 1
|
get() = index + 1
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ interface SManga : Serializable {
|
|||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val LICENSED = 3
|
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 {
|
fun create(): SManga {
|
||||||
return SMangaImpl()
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.log.maybeInjectEHLogger
|
||||||
import exh.patch.injectPatches
|
import exh.patch.injectPatches
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import java.net.URI
|
|
||||||
import java.net.URISyntaxException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -25,6 +23,9 @@ import okhttp3.Response
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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.
|
* A simple implementation for sources from a website.
|
||||||
@@ -36,22 +37,24 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*/
|
*/
|
||||||
// SY -->
|
// SY -->
|
||||||
protected val network: NetworkHelper by lazy {
|
protected val network: NetworkHelper by lazy {
|
||||||
val original = Injekt.get<NetworkHelper>()
|
val network = Injekt.get<NetworkHelper>()
|
||||||
object : NetworkHelper(Injekt.get<Application>()) {
|
object : NetworkHelper(Injekt.get<Application>()) {
|
||||||
override val client: OkHttpClient
|
override val client: OkHttpClient
|
||||||
get() = delegate?.networkHttpClient ?: original.client
|
get() = delegate?.networkHttpClient ?: network.client
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.injectPatches { id }
|
.injectPatches { id }
|
||||||
|
.maybeInjectEHLogger()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override val cloudflareClient: OkHttpClient
|
override val cloudflareClient: OkHttpClient
|
||||||
get() = delegate?.networkCloudflareClient ?: original.cloudflareClient
|
get() = delegate?.networkCloudflareClient ?: network.cloudflareClient
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.injectPatches { id }
|
.injectPatches { id }
|
||||||
|
.maybeInjectEHLogger()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override val cookieManager: AndroidCookieJar
|
override val cookieManager: AndroidCookieJar
|
||||||
get() = original.cookieManager
|
get() = network.cookieManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -88,7 +91,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Headers used for requests.
|
* 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.
|
* 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.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LEWD!
|
* LEWD!
|
||||||
*/
|
*/
|
||||||
interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||||
val db: DatabaseHelper get() = Injekt.get()
|
val db: DatabaseHelper get() = Injekt.get()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,8 +55,7 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
Single.fromCallable {
|
Single.fromCallable {
|
||||||
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
||||||
}.map {
|
}.map {
|
||||||
if (it != null) it.raise(metaClass)
|
it?.raise(metaClass) ?: newMetaInstance()
|
||||||
else newMetaInstance()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Single.just(newMetaInstance())
|
Single.just(newMetaInstance())
|
||||||
@@ -112,17 +111,14 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
||||||
|
|
||||||
companion object {
|
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<*, *>? {
|
fun Source.getMetadataSource(): MetadataSource<*, *>? {
|
||||||
return if (!this.isLewdSource()) {
|
return when {
|
||||||
null
|
!this.isMetadataSource() -> null
|
||||||
} else if (this is LewdSource<*, *>) {
|
this is MetadataSource<*, *> -> this
|
||||||
this
|
this is EnhancedHttpSource && this.enhancedSource is MetadataSource<*, *> -> this.enhancedSource
|
||||||
} else if (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>) {
|
else -> null
|
||||||
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.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.annoations.Nsfw
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -47,12 +49,12 @@ import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
|||||||
import exh.util.UriFilter
|
import exh.util.UriFilter
|
||||||
import exh.util.UriGroup
|
import exh.util.UriGroup
|
||||||
import exh.util.asObservableWithAsyncStacktrace
|
import exh.util.asObservableWithAsyncStacktrace
|
||||||
|
import exh.util.dropBlank
|
||||||
import exh.util.ignore
|
import exh.util.ignore
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
|
import exh.util.trimAll
|
||||||
import exh.util.trimOrNull
|
import exh.util.trimOrNull
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.util.ArrayList
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
@@ -68,16 +70,19 @@ import org.jsoup.nodes.TextNode
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
// TODO Consider gallery updating when doing tabbed browsing
|
// TODO Consider gallery updating when doing tabbed browsing
|
||||||
|
@Nsfw
|
||||||
class EHentai(
|
class EHentai(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
val exh: Boolean,
|
val exh: Boolean,
|
||||||
val context: Context
|
val context: Context
|
||||||
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
) : HttpSource(), MetadataSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
||||||
override val metaClass = EHentaiSearchMetadata::class
|
override val metaClass = EHentaiSearchMetadata::class
|
||||||
|
|
||||||
val domain: String
|
private val domain: String
|
||||||
get() = if (exh) {
|
get() = if (exh) {
|
||||||
"exhentai.org"
|
"exhentai.org"
|
||||||
} else {
|
} else {
|
||||||
@@ -88,9 +93,9 @@ class EHentai(
|
|||||||
get() = "https://$domain"
|
get() = "https://$domain"
|
||||||
|
|
||||||
override val lang = "all"
|
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()
|
private val updateHelper: EHentaiUpdateHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,11 +108,11 @@ class EHentai(
|
|||||||
val parsedMangas = select(".itg > tbody > tr").filter {
|
val parsedMangas = select(".itg > tbody > tr").filter {
|
||||||
// Do not parse header and ads
|
// Do not parse header and ads
|
||||||
it.selectFirst("th") == null && it.selectFirst(".itd") == null
|
it.selectFirst("th") == null && it.selectFirst(".itd") == null
|
||||||
}.map {
|
}.map { body ->
|
||||||
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
val thumbnailElement = body.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||||
val column2 = it.selectFirst(".gl3e, .gl2c")
|
val column2 = body.selectFirst(".gl3e, .gl2c")
|
||||||
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
val linkElement = body.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||||
val infoElement = it.selectFirst(".gl3e")
|
val infoElement = body.selectFirst(".gl3e")
|
||||||
|
|
||||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||||
val infoElements = infoElement?.select("div")
|
val infoElements = infoElement?.select("div")
|
||||||
@@ -142,7 +147,7 @@ class EHentai(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val tagElement = it.selectFirst(".gl3c > a")
|
val tagElement = body.selectFirst(".gl3c > a")
|
||||||
val tagElements = tagElement.select("div")
|
val tagElements = tagElement.select("div")
|
||||||
tagElements.forEach { element ->
|
tagElements.forEach { element ->
|
||||||
if (element.className() == "gt") {
|
if (element.className() == "gt") {
|
||||||
@@ -172,11 +177,11 @@ class EHentai(
|
|||||||
|
|
||||||
getPageCount(infoElements.getOrNull(5))?.let { length = it }
|
getPageCount(infoElements.getOrNull(5))?.let { length = it }
|
||||||
} else {
|
} else {
|
||||||
val parsedGenre = it.selectFirst(".gl1c div")
|
val parsedGenre = body.selectFirst(".gl1c div")
|
||||||
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
|
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
|
||||||
|
|
||||||
val info = it.selectFirst(".gl2c")
|
val info = body.selectFirst(".gl2c")
|
||||||
val extraInfo = it.selectFirst(".gl4c")
|
val extraInfo = body.selectFirst(".gl4c")
|
||||||
|
|
||||||
val infoList = info.select("div div")
|
val infoList = info.select("div div")
|
||||||
|
|
||||||
@@ -392,7 +397,7 @@ class EHentai(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||||
client.newCall(it).asObservableSuccess()
|
client.newCall(it).asObservableSuccess()
|
||||||
@@ -441,14 +446,14 @@ class EHentai(
|
|||||||
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
||||||
override fun latestUpdatesParse(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(
|
return GET(
|
||||||
page?.let {
|
page?.let {
|
||||||
addParam(url, "page", Integer.toString(page - 1))
|
addParam(url, "page", Integer.toString(page - 1))
|
||||||
} ?: url,
|
} ?: url,
|
||||||
additionalHeaders?.let {
|
additionalHeaders?.let { additionalHeadersNotNull ->
|
||||||
val headers = headers.newBuilder()
|
val headers = headers.newBuilder()
|
||||||
it.toMultimap().forEach { (t, u) ->
|
additionalHeadersNotNull.toMultimap().forEach { (t, u) ->
|
||||||
u.forEach {
|
u.forEach {
|
||||||
headers.add(t, it)
|
headers.add(t, it)
|
||||||
}
|
}
|
||||||
@@ -597,12 +602,10 @@ class EHentai(
|
|||||||
RaisedTag(
|
RaisedTag(
|
||||||
namespace,
|
namespace,
|
||||||
element.text().trim(),
|
element.text().trim(),
|
||||||
if (element.hasClass("gtl")) {
|
when {
|
||||||
TAG_TYPE_LIGHT
|
element.hasClass("gtl") -> TAG_TYPE_LIGHT
|
||||||
} else if (element.hasClass("gtw")) {
|
element.hasClass("gtw") -> TAG_TYPE_WEAK
|
||||||
TAG_TYPE_WEAK
|
else -> TAG_TYPE_NORMAL
|
||||||
} else {
|
|
||||||
TAG_TYPE_NORMAL
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -627,7 +630,7 @@ class EHentai(
|
|||||||
.map { realImageUrlParse(it, page) }
|
.map { realImageUrlParse(it, page) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun realImageUrlParse(response: Response, page: Page): String {
|
private fun realImageUrlParse(response: Response, page: Page): String {
|
||||||
with(response.asJsoup()) {
|
with(response.asJsoup()) {
|
||||||
val currentImage = getElementById("img").attr("src")
|
val currentImage = getElementById("img").attr("src")
|
||||||
// Each press of the retry button will choose another server
|
// Each press of the retry button will choose another server
|
||||||
@@ -678,30 +681,30 @@ class EHentai(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun spPref() = if (exh) {
|
fun spPref() = if (exh) {
|
||||||
prefs.eh_exhSettingsProfile()
|
preferences.eh_exhSettingsProfile()
|
||||||
} else {
|
} 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()
|
val cookies: MutableMap<String, String> = mutableMapOf()
|
||||||
if (prefs.enableExhentai().get()) {
|
if (preferences.enableExhentai().get()) {
|
||||||
cookies[LoginController.MEMBER_ID_COOKIE] = prefs.memberIdVal().get()
|
cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
|
||||||
cookies[LoginController.PASS_HASH_COOKIE] = prefs.passHashVal().get()
|
cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get()
|
||||||
cookies[LoginController.IGNEOUS_COOKIE] = prefs.igneousVal().get()
|
cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get()
|
||||||
cookies["sp"] = sp.toString()
|
cookies["sp"] = sp.toString()
|
||||||
|
|
||||||
val sessionKey = prefs.eh_settingsKey().get()
|
val sessionKey = preferences.eh_settingsKey().get()
|
||||||
if (sessionKey.isNotBlank()) {
|
if (sessionKey.isNotBlank()) {
|
||||||
cookies["sk"] = sessionKey
|
cookies["sk"] = sessionKey
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionCookie = prefs.eh_sessionCookie().get()
|
val sessionCookie = preferences.eh_sessionCookie().get()
|
||||||
if (sessionCookie.isNotBlank()) {
|
if (sessionCookie.isNotBlank()) {
|
||||||
cookies["s"] = sessionCookie
|
cookies["s"] = sessionCookie
|
||||||
}
|
}
|
||||||
|
|
||||||
val hathPerksCookie = prefs.eh_hathPerksCookies().get()
|
val hathPerksCookie = preferences.eh_hathPerksCookies().get()
|
||||||
if (hathPerksCookie.isNotBlank()) {
|
if (hathPerksCookie.isNotBlank()) {
|
||||||
cookies["hath_perks"] = hathPerksCookie
|
cookies["hath_perks"] = hathPerksCookie
|
||||||
}
|
}
|
||||||
@@ -721,7 +724,7 @@ class EHentai(
|
|||||||
// Headers
|
// Headers
|
||||||
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())
|
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()
|
.buildUpon()
|
||||||
.appendQueryParameter(param, value)
|
.appendQueryParameter(param, value)
|
||||||
.toString()
|
.toString()
|
||||||
@@ -746,9 +749,10 @@ class EHentai(
|
|||||||
return FilterList(
|
return FilterList(
|
||||||
AutoCompleteTags(
|
AutoCompleteTags(
|
||||||
EHTags.getNameSpaces().map { "$it:" } + EHTags.getAllTags(),
|
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)
|
Watched(isEnabled = true)
|
||||||
} else {
|
} else {
|
||||||
Watched(isEnabled = false)
|
Watched(isEnabled = false)
|
||||||
@@ -816,14 +820,13 @@ class EHentai(
|
|||||||
private fun combineQuery(filters: FilterList): String {
|
private fun combineQuery(filters: FilterList): String {
|
||||||
val stringBuilder = StringBuilder()
|
val stringBuilder = StringBuilder()
|
||||||
val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter ->
|
val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter ->
|
||||||
val splitState = filter.state.map(String::trim).filterNot(String::isBlank)
|
val splitState = filter.state.trimAll().dropBlank()
|
||||||
splitState.mapNotNull { tag ->
|
splitState.mapNotNull { tag ->
|
||||||
val split = tag.split(":").filterNot { it.isBlank() }.toMutableList()
|
val split = tag.split(":").filterNot { it.isBlank() }
|
||||||
if (split.size > 1) {
|
if (split.size > 1) {
|
||||||
val namespace = split[0].removePrefix("-")
|
val namespace = split[0].removePrefix("-")
|
||||||
val exclude = split[0].startsWith("-")
|
val exclude = split[0].startsWith("-")
|
||||||
split -= namespace
|
AdvSearchEntry(Pair(namespace, split[1]), exclude)
|
||||||
AdvSearchEntry(Pair(namespace, split.joinToString(":")), exclude)
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -865,7 +868,7 @@ class EHentai(
|
|||||||
UriFilter {
|
UriFilter {
|
||||||
override fun addToUri(builder: Uri.Builder) {
|
override fun addToUri(builder: Uri.Builder) {
|
||||||
if (state > 0) {
|
if (state > 0) {
|
||||||
builder.appendQueryParameter("f_srdd", Integer.toString(state + 1))
|
builder.appendQueryParameter("f_srdd", (state + 1).toString())
|
||||||
builder.appendQueryParameter("f_sr", "on")
|
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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -18,19 +18,17 @@ import exh.metadata.metadata.base.RaisedTag
|
|||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class Hitomi(delegate: HttpSource, val context: Context) :
|
class Hitomi(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<HitomiSearchMetadata, Document>,
|
MetadataSource<HitomiSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val metaClass = HitomiSearchMetadata::class
|
override val metaClass = HitomiSearchMetadata::class
|
||||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
override val lang = if (id == otherId) "all" else delegate.lang
|
||||||
override val id: Long
|
|
||||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
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() }
|
characters = content.select("a").map { it.text() }
|
||||||
tags += characters.map {
|
tags += characters.map {
|
||||||
RaisedTag(
|
RaisedTag(
|
||||||
"character", it,
|
"character",
|
||||||
|
it,
|
||||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
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 if (it.attr("href").startsWith("/tag/female")) "female"
|
||||||
else "misc"
|
else "misc"
|
||||||
RaisedTag(
|
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
|
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(
|
override val matchingHosts = listOf(
|
||||||
"hitomi.la"
|
"hitomi.la"
|
||||||
|
|||||||
@@ -1,40 +1,268 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.preference.PreferenceScreen
|
import com.bluelinelabs.conductor.Controller
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.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.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.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.source.DelegatedHttpSource
|
||||||
|
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
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 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) :
|
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
ConfigurableSource,
|
MetadataSource<MangaDexSearchMetadata, Response>,
|
||||||
UrlImportableSource {
|
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")
|
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> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
importIdToMdId(query) {
|
||||||
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||||
|
|
||||||
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
|
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
|
||||||
"/manga/${uri.pathSegments[1]}"
|
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.elvishew.xlog.XLog
|
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.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.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_SOURCE_ID
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
import exh.util.asFlow
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
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.map
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.single
|
||||||
import kotlinx.coroutines.rx2.asFlowable
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
import kotlinx.coroutines.rx2.asSingle
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
// TODO LocalSource compatibility
|
class MergedSource : SuspendHttpSource() {
|
||||||
// TODO Disable clear database option
|
|
||||||
class MergedSource : HttpSource() {
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val sourceManager: SourceManager 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 id: Long = MERGED_SOURCE_ID
|
||||||
|
|
||||||
override val baseUrl = ""
|
override val baseUrl = ""
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
override suspend fun popularMangaRequestSuspended(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
override suspend fun popularMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
override suspend fun searchMangaRequestSuspended(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesParse(response: Response) = 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> {
|
override fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
|
||||||
return RxJavaInterop.toV1Observable(
|
return flow {
|
||||||
readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded ->
|
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 {
|
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
|
url = manga.url
|
||||||
}
|
}
|
||||||
}.asFlowable()
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
|
||||||
return RxJavaInterop.toV1Single(
|
// TODO more chapter dedupe
|
||||||
GlobalScope.async(Dispatchers.IO) {
|
return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable()
|
||||||
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer()
|
.asFlow()
|
||||||
loadedMangas.map { loadedManga ->
|
.map { chapterList ->
|
||||||
async(Dispatchers.IO) {
|
val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() }
|
||||||
loadedManga.source.fetchChapterList(loadedManga.manga).map { chapterList ->
|
val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId }
|
||||||
chapterList.map { chapter ->
|
if (editScanlators) {
|
||||||
chapter.apply {
|
chapterList.onEach { chapter ->
|
||||||
url = writeUrlConfig(UrlConfig(loadedManga.source.id, url, loadedManga.manga.url))
|
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())
|
}
|
||||||
}
|
} else {
|
||||||
}.buffer().map { it.await() }.toList().flatten()
|
emptyList<Pair<List<Chapter>, List<Chapter>>>().asFlow()
|
||||||
}.asSingle(Dispatchers.IO)
|
}
|
||||||
).toObservable()
|
}
|
||||||
|
}.buffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource {
|
||||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await()
|
||||||
|
val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId)
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
if (manga == null) {
|
||||||
val config = readUrlConfig(chapter.url)
|
manga = Manga.create(reference.mangaSourceId).apply {
|
||||||
val source = sourceManager.getOrStub(config.source)
|
url = reference.mangaUrl
|
||||||
return source.fetchPageList(
|
|
||||||
SChapter.create().apply {
|
|
||||||
copyFrom(chapter)
|
|
||||||
url = config.url
|
|
||||||
}
|
}
|
||||||
).map { pages ->
|
manga.copyFrom(source.fetchMangaDetails(manga).asFlow().single())
|
||||||
pages.map { page ->
|
try {
|
||||||
page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl)))
|
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> {
|
data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
override val supportsLatest = false
|
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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||||
@@ -25,14 +25,12 @@ import exh.util.urlImportFetchSearchManga
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class NHentai(delegate: HttpSource, val context: Context) :
|
class NHentai(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<NHentaiSearchMetadata, Response>,
|
MetadataSource<NHentaiSearchMetadata, Response>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val metaClass = NHentaiSearchMetadata::class
|
override val metaClass = NHentaiSearchMetadata::class
|
||||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
override val lang = if (id == otherId) "all" else delegate.lang
|
||||||
override val id: Long
|
|
||||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
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(
|
override val matchingHosts = listOf(
|
||||||
"nhentai.net"
|
"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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -24,7 +24,7 @@ import rx.Observable
|
|||||||
|
|
||||||
class PervEden(delegate: HttpSource, val context: Context) :
|
class PervEden(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<PervEdenSearchMetadata, Document>,
|
MetadataSource<PervEdenSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val metaClass = PervEdenSearchMetadata::class
|
override val metaClass = PervEdenSearchMetadata::class
|
||||||
override val lang = delegate.lang
|
override val lang = delegate.lang
|
||||||
@@ -77,7 +77,8 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
|||||||
if (it is Element && it.tagName() == "a") {
|
if (it is Element && it.tagName() == "a") {
|
||||||
artist = it.text()
|
artist = it.text()
|
||||||
tags += RaisedTag(
|
tags += RaisedTag(
|
||||||
"artist", it.text().toLowerCase(),
|
"artist",
|
||||||
|
it.text().toLowerCase(),
|
||||||
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
|
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,8 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
|||||||
"Genres" -> {
|
"Genres" -> {
|
||||||
if (it is Element && it.tagName() == "a") {
|
if (it is Element && it.tagName() == "a") {
|
||||||
tags += RaisedTag(
|
tags += RaisedTag(
|
||||||
null, it.text().toLowerCase(),
|
null,
|
||||||
|
it.text().toLowerCase(),
|
||||||
PervEdenSearchMetadata.TAG_TYPE_DEFAULT
|
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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -22,7 +22,7 @@ import rx.Observable
|
|||||||
|
|
||||||
class EightMuses(delegate: HttpSource, val context: Context) :
|
class EightMuses(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<EightMusesSearchMetadata, Document>,
|
MetadataSource<EightMusesSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val metaClass = EightMusesSearchMetadata::class
|
override val metaClass = EightMusesSearchMetadata::class
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
@@ -65,8 +65,8 @@ class EightMuses(delegate: HttpSource, val context: Context) :
|
|||||||
thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
|
thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
|
||||||
?.selectFirst(".lazyload")
|
?.selectFirst(".lazyload")
|
||||||
?.attr("data-src")?.let {
|
?.attr("data-src")?.let {
|
||||||
baseUrl + it
|
baseUrl + it
|
||||||
}
|
}
|
||||||
|
|
||||||
tags.clear()
|
tags.clear()
|
||||||
tags += RaisedTag(
|
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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -22,7 +22,7 @@ import rx.Observable
|
|||||||
|
|
||||||
class HBrowse(delegate: HttpSource, val context: Context) :
|
class HBrowse(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<HBrowseSearchMetadata, Document>,
|
MetadataSource<HBrowseSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val metaClass = HBrowseSearchMetadata::class
|
override val metaClass = HBrowseSearchMetadata::class
|
||||||
override val lang = "en"
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -24,7 +24,7 @@ import rx.Observable
|
|||||||
|
|
||||||
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<HentaiCafeSearchMetadata, Document>,
|
MetadataSource<HentaiCafeSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
/**
|
/**
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
* 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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -24,7 +24,7 @@ import rx.Observable
|
|||||||
|
|
||||||
class Pururin(delegate: HttpSource, val context: Context) :
|
class Pururin(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<PururinSearchMetadata, Document>,
|
MetadataSource<PururinSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
/**
|
/**
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
* 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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@@ -20,14 +20,14 @@ import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
|
|||||||
import exh.util.dropBlank
|
import exh.util.dropBlank
|
||||||
import exh.util.trimAll
|
import exh.util.trimAll
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class Tsumino(delegate: HttpSource, val context: Context) :
|
class Tsumino(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<TsuminoSearchMetadata, Document>,
|
MetadataSource<TsuminoSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
override val metaClass = TsuminoSearchMetadata::class
|
override val metaClass = TsuminoSearchMetadata::class
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import eu.kanade.tachiyomi.R
|
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.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
|
|
||||||
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
|||||||
when (preferences.themeDark().get()) {
|
when (preferences.themeDark().get()) {
|
||||||
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
|
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
|
||||||
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
||||||
|
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
|
||||||
else -> R.style.Theme_Tachiyomi_Dark
|
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
|
lateinit var binding: VB
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addLifecycleListener(object : LifecycleListener() {
|
addLifecycleListener(
|
||||||
override fun postCreateView(controller: Controller, view: View) {
|
object : LifecycleListener() {
|
||||||
onViewCreated(view)
|
override fun postCreateView(controller: Controller, view: View) {
|
||||||
}
|
onViewCreated(view)
|
||||||
|
}
|
||||||
|
|
||||||
override fun preCreateView(controller: Controller) {
|
override fun preCreateView(controller: Controller) {
|
||||||
Timber.d("Create view for ${controller.instance()}")
|
Timber.d("Create view for ${controller.instance()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun preAttach(controller: Controller, view: View) {
|
override fun preAttach(controller: Controller, view: View) {
|
||||||
Timber.d("Attach view for ${controller.instance()}")
|
Timber.d("Attach view for ${controller.instance()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun preDetach(controller: Controller, view: View) {
|
override fun preDetach(controller: Controller, view: View) {
|
||||||
Timber.d("Detach view for ${controller.instance()}")
|
Timber.d("Detach view for ${controller.instance()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun preDestroyView(controller: Controller, view: View) {
|
override fun preDestroyView(controller: Controller, view: View) {
|
||||||
Timber.d("Destroy view for ${controller.instance()}")
|
Timber.d("Destroy view for ${controller.instance()}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val containerView: View?
|
override val containerView: View?
|
||||||
@@ -98,17 +100,19 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
var expandActionViewFromInteraction = false
|
var expandActionViewFromInteraction = false
|
||||||
|
|
||||||
fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) {
|
fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) {
|
||||||
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
setOnActionExpandListener(
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
object : MenuItem.OnActionExpandListener {
|
||||||
return onExpand?.invoke(item) ?: true
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
}
|
return onExpand?.invoke(item) ?: true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
|
|
||||||
return onCollapse?.invoke(item) ?: true
|
return onCollapse?.invoke(item) ?: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
if (expandActionViewFromInteraction) {
|
if (expandActionViewFromInteraction) {
|
||||||
expandActionViewFromInteraction = false
|
expandActionViewFromInteraction = false
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.bluelinelabs.conductor.Router
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
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 {
|
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||||
val controller = getControllerWithTag(tag)
|
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)
|
return RouterTransaction.with(this)
|
||||||
.pushChangeHandler(FadeChangeHandler(duration))
|
.pushChangeHandler(OneWayFadeChangeHandler())
|
||||||
.popChangeHandler(FadeChangeHandler(duration))
|
.popChangeHandler(OneWayFadeChangeHandler())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private typealias ExtensionTuple =
|
private typealias ExtensionTuple =
|
||||||
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
||||||
|
|||||||