Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51a109285a | |||
| 02601aa32a | |||
| 60e520e37e | |||
| 3fb4b565fa | |||
| b2e33ab950 | |||
| acad70d8db | |||
| ebc1f2bb41 | |||
| 391e477e04 | |||
| 5fa3d55bc9 | |||
| ef02bd112c | |||
| ae7d94a1f2 | |||
| 0a96252ce8 | |||
| 7907723623 | |||
| 8d3c0199b4 | |||
| d5fcded898 | |||
| 674a9c5067 | |||
| 1f37d571cf | |||
| 36c45c9450 | |||
| cc2f976c81 | |||
| d92e790c5e | |||
| 7d659f559e | |||
| 3a20e24ad0 | |||
| 9a97a97aa7 | |||
| 84076c2582 | |||
| b67d2bba40 | |||
| 9fa278f708 | |||
| 56ea025e20 | |||
| dab002cf4d | |||
| bca6f39a33 | |||
| fbfad27c27 | |||
| a93129c4f2 | |||
| 89531cd549 | |||
| e50f04ec54 | |||
| d8b40c2dc4 | |||
| d62de90b77 | |||
| ab14ce0d75 | |||
| f2efe49ea1 | |||
| 2eb5436b25 | |||
| fba4bd163b | |||
| ae073f9207 | |||
| 1c084d42df | |||
| f245bbcfeb | |||
| 29310c86e2 | |||
| f492ad2529 | |||
| 33b6912c22 | |||
| 9e63f32a82 | |||
| 834e3d78ab | |||
| 2d2378a1e2 | |||
| 3cc61bc3b8 | |||
| 6b771b4a70 | |||
| 9c2eadd8ca | |||
| f09c977661 | |||
| 2acc364960 | |||
| c63300d9ad | |||
| c90aec5c3d | |||
| 8e6f04f258 | |||
| 354c61cf59 | |||
| 606101dc8b | |||
| 97eda86ac4 | |||
| dd4384767c | |||
| b18075de14 | |||
| c39ec81f42 | |||
| 99d23a47b7 | |||
| 481f600056 | |||
| 143d0d2518 | |||
| 38fef11287 | |||
| 9adbb1b115 | |||
| ec9967d2d6 | |||
| ba0e353ea1 | |||
| e5131e1985 | |||
| 43555b3b1a | |||
| 3419bebb70 | |||
| b13ca1a097 | |||
| dd914047f8 | |||
| 666447faac | |||
| e9a21a6bbe | |||
| de4e0abef4 | |||
| ac7e2909ba | |||
| 14de1973a3 | |||
| 3053bf9d5d | |||
| 3192140421 | |||
| 80cf38a70d | |||
| 42bbf07859 | |||
| 9b4aac7ce5 | |||
| ab64e51c6b | |||
| 82a92b9497 | |||
| a855c4a929 | |||
| de6c428d72 | |||
| a34133f526 | |||
| 56d127003f | |||
| 323ce4dbab | |||
| a802eb1cca | |||
| 3bb005acb2 | |||
| d8ee654a65 | |||
| 2b075b5a39 | |||
| e085855ec6 | |||
| 1518c2aa25 | |||
| 623b64aa79 | |||
| 2f9069765e | |||
| eaa2bb22ed | |||
| 1389649553 | |||
| b3ddc1dfa3 | |||
| d55802a2f2 | |||
| 08cb46f6cd | |||
| 15482914ef | |||
| 3e5e983b9e | |||
| 36484bc349 | |||
| 85362e2030 | |||
| 3ebdd7b351 | |||
| 2a32c9cb5d | |||
| 677f93a6ec | |||
| 584f7b516f | |||
| 7d49479783 | |||
| 970e6e2560 | |||
| 19d539b197 | |||
| 97e47b98b1 | |||
| 844fb8129c | |||
| 7a9c2afe87 | |||
| 35879921dd | |||
| f5a90d46ec | |||
| d332f98b34 | |||
| b339bd4f3f | |||
| de414fb49b | |||
| 185b7fe70e | |||
| a947fcd9b9 | |||
| 7b9f5474bb | |||
| 6238f06d39 | |||
| 7e612e63b4 | |||
| 63139a5c08 | |||
| b63df25f7b | |||
| 44385ed9cc | |||
| 9eb1927d2e | |||
| 20cbadb23d | |||
| e37d4afce6 | |||
| fbc98ddb0a | |||
| 91ca176c28 | |||
| fd65db51c1 | |||
| 33a590d895 | |||
| bc871cd2ee | |||
| 65f66630cf | |||
| ab0f5d107f | |||
| 4a2d9dbdf8 | |||
| 08f1eff450 | |||
| e500d0bebf | |||
| 2629b3420b | |||
| 81e7d674a2 | |||
| 0c33b7915b | |||
| 29e4392490 | |||
| ba9db7ceb9 | |||
| 32197b1491 | |||
| 3f56c81c03 | |||
| b657af8d1c | |||
| 273d61e69b | |||
| 2ec5581e8c | |||
| be6637c7fd | |||
| 93d317629f | |||
| df188b7b90 | |||
| 27f2e8ecbc | |||
| 44ba757ad8 | |||
| d04cdd9b34 | |||
| 333f55a44b | |||
| 1f9b69fc07 | |||
| 09802c3609 | |||
| 93fe927de2 | |||
| 428a9e82f3 | |||
| 7e1389ef05 | |||
| fc354f5792 | |||
| c26adbb81d | |||
| 31473351af | |||
| 5c2d26aa7c | |||
| f0a2b85dd5 | |||
| 85398f7c30 | |||
| c787498b85 | |||
| 8532a9e2c5 | |||
| aa6013b7ca | |||
| cb2432cce9 | |||
| dd81e5f2b9 | |||
| c3be087472 | |||
| 84a1da2952 | |||
| e62de734aa | |||
| 866a92474f | |||
| b297d580b0 | |||
| 858a5e6eee | |||
| 7c3c452ac2 | |||
| b9b5ef55ab | |||
| 8686fecb1f | |||
| c5148b4739 | |||
| a4933388aa | |||
| 9095c98159 | |||
| 8c2de86b16 | |||
| 635dd0cda5 | |||
| bde5d4da26 | |||
| 4f02f652d9 | |||
| 6dad90e19c | |||
| 8fdd6c3bf9 | |||
| 4cbf647365 | |||
| 86d67b9bf7 | |||
| 69aebb5571 | |||
| 00afc11d4f | |||
| c518b593ce | |||
| 5c352cb3c0 | |||
| 73c9df9c43 | |||
| 8b2f24c86a | |||
| 39c61a77b1 | |||
| d09bcafe7d | |||
| 1fdf2225d9 | |||
| b4a226157c | |||
| 39d6319e8f | |||
| 765eac843d | |||
| 9a6b8a3f41 | |||
| 28fab7a918 | |||
| 900aa155ca | |||
| f9b4cb5bc9 | |||
| 1147bab1ce | |||
| 0aebe1da43 | |||
| f45fdca168 | |||
| fc5eb4cccc | |||
| 8ac309c4ae | |||
| f170446c5f | |||
| 643bec9bbb | |||
| 134be3893e | |||
| 5855822edd | |||
| 3343b766a2 | |||
| 329d24c7db | |||
| bdfbc641d9 | |||
| 6e570d7fad | |||
| b5d696ebe2 | |||
| 5299ae4856 | |||
| a9038831da | |||
| f1a8132307 | |||
| 76185338bf | |||
| bda4aae83d | |||
| 80bf908133 | |||
| 91b49f8a0c | |||
| 80a5a54e60 | |||
| 3104f3a8b5 | |||
| 4fa2c968a9 |
@@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.6.1)
|
- To the latest version of the app (stable is v1.7.0)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ labels: "bug"
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.6.1)
|
- To the latest version of the app (stable is v1.7.0)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ labels: "feature"
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.6.1)
|
- To the latest version of the app (stable is v1.7.0)
|
||||||
- All extensions
|
- All extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Autoclose issues
|
- name: Autoclose issues
|
||||||
uses: arkon/issue-closer-action@v3.0
|
uses: arkon/issue-closer-action@v3.4
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
rules: |
|
rules: |
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: Issue moderator
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
moderate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Moderate issues
|
||||||
|
uses: tachiyomiorg/issue-moderator-action@v1.0
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# TachiyomiSY
|
# TachiyomiSY
|
||||||
Tachiyomi is a free and open source manga reader for Android 5.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
+31
-37
@@ -8,11 +8,11 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("kapt")
|
|
||||||
kotlin("plugin.parcelize")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
// Realm (EH)
|
// Realm (EH)
|
||||||
|
kotlin("kapt")
|
||||||
id("realm-android")
|
id("realm-android")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ android {
|
|||||||
minSdkVersion(AndroidConfig.minSdk)
|
minSdkVersion(AndroidConfig.minSdk)
|
||||||
targetSdkVersion(AndroidConfig.targetSdk)
|
targetSdkVersion(AndroidConfig.targetSdk)
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode = 15
|
versionCode = 19
|
||||||
versionName = "1.6.1"
|
versionName = "1.7.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -62,14 +62,12 @@ android {
|
|||||||
applicationIdSuffix = ".rt"
|
applicationIdSuffix = ".rt"
|
||||||
//isMinifyEnabled = true
|
//isMinifyEnabled = true
|
||||||
//isShrinkResources = true
|
//isShrinkResources = true
|
||||||
isZipAlignEnabled = true
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
isZipAlignEnabled = true
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,34 +122,34 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
implementation("org.tachiyomi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||||
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
|
implementation("androidx.appcompat:appcompat:1.4.0-alpha01")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.browser:browser:1.3.0")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
implementation("androidx.core:core-ktx:1.3.2")
|
implementation("androidx.core:core-ktx:1.6.0-beta01")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.3.0"
|
val lifecycleVersion = "2.4.0-alpha01"
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
||||||
|
|
||||||
// UI library
|
// UI library
|
||||||
implementation("com.google.android.material:material:1.3.0")
|
implementation("com.google.android.material:material:1.4.0-beta01")
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
|
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
@@ -160,7 +158,7 @@ dependencies {
|
|||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "5.0.0-alpha.2"
|
val okhttpVersion = "4.9.1"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
@@ -191,10 +189,10 @@ dependencies {
|
|||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("io.requery:sqlite-android:3.33.0")
|
implementation("com.github.requery:sqlite-android:3.35.5")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
val nucleusVersion = "3.0.0"
|
val nucleusVersion = "3.0.0"
|
||||||
@@ -205,12 +203,14 @@ dependencies {
|
|||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
val glideVersion = "4.12.0"
|
val coilVersion = "1.2.0"
|
||||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
implementation("io.coil-kt:coil:$coilVersion")
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||||
|
exclude(module = "image-decoder")
|
||||||
|
}
|
||||||
|
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
@@ -222,14 +222,13 @@ dependencies {
|
|||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
|
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
|
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||||
|
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||||
val materialDialogsVersion = "3.1.1"
|
val materialDialogsVersion = "3.1.1"
|
||||||
@@ -245,7 +244,7 @@ dependencies {
|
|||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "0.12.0"
|
val flowbindingVersion = "1.0.0"
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||||
@@ -267,7 +266,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
val coroutinesVersion = "1.4.3"
|
val coroutinesVersion = "1.5.0"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
@@ -285,8 +284,8 @@ dependencies {
|
|||||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
|
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0")
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
|
implementation("com.google.firebase:firebase-crashlytics-ktx:18.0.0")
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation("com.elvishew:xlog:1.9.0")
|
implementation("com.elvishew:xlog:1.9.0")
|
||||||
@@ -299,13 +298,7 @@ dependencies {
|
|||||||
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
||||||
|
|
||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||||
|
|
||||||
// JsonReader for similar manga
|
|
||||||
implementation("com.squareup.moshi:moshi:1.12.0")
|
|
||||||
|
|
||||||
implementation("com.mikepenz:fastadapter:5.4.0")
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -318,7 +311,8 @@ tasks {
|
|||||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-allowaccessmodification
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
-verbose
|
||||||
|
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keep class androidx.annotation.Keep
|
||||||
|
|
||||||
|
-keep @androidx.annotation.Keep class * {*;}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <init>(...);
|
||||||
|
}
|
||||||
Vendored
+15
-42
@@ -58,6 +58,7 @@
|
|||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Filter serializer
|
||||||
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
|
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
|
||||||
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
|
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
|
||||||
*** Companion;
|
*** Companion;
|
||||||
@@ -66,37 +67,22 @@
|
|||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Madokami extension username and password crash fix
|
# Keep extension's common dependencies
|
||||||
-keepclassmembers class androidx.preference.EditTextPreference {
|
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||||
*** mOnBindEditTextListener;
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
*** mText;
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
public *;
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
}
|
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
# Hitomi extension crash fix
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
-keepclassmembers class rx.Single {
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
*** onSubscribe;
|
-keep,allowoptimization class androidx.preference.** { *; }
|
||||||
final *;
|
-keep,allowoptimization class okio.** { *; }
|
||||||
protected *;
|
-keep,allowoptimization class kotlinx.serialization.** { *; }
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
|
||||||
long producerIndex;
|
|
||||||
long consumerIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
|
||||||
rx.internal.util.atomic.LinkedQueueNode producerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
|
|
||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
-dontnote rx.internal.util.PlatformDependent
|
-dontnote rx.internal.util.PlatformDependent
|
||||||
|
|
||||||
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
||||||
@@ -133,8 +119,9 @@
|
|||||||
# Application classes that will be serialized/deserialized over Gson
|
# Application classes that will be serialized/deserialized over Gson
|
||||||
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
# Prevent proguard from stripping interface information from TypeAdapterFactory, TypeAdapter,
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
|
-keep class * extends com.google.gson.TypeAdapter
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
-keep class * implements com.google.gson.JsonSerializer
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
-keep class * implements com.google.gson.JsonDeserializer
|
||||||
@@ -155,20 +142,6 @@
|
|||||||
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
|
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
|
||||||
-keep class uy.kohesive.injekt.** { *; }
|
-keep class uy.kohesive.injekt.** { *; }
|
||||||
|
|
||||||
|
|
||||||
# === Glide
|
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
|
||||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
|
||||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
|
|
||||||
|
|
||||||
# === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt
|
|
||||||
-dontwarn jp.co.cyberagent.android.gpuimage.**
|
|
||||||
|
|
||||||
# === Conductor
|
# === Conductor
|
||||||
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
|
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
|
||||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_updated"
|
android:shortcutId="show_recently_updated"
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
android:shortcutLongLabel="@string/label_recent_updates"
|
||||||
android:shortcutShortLabel="@string/short_recent_updates">
|
android:shortcutShortLabel="@string/label_recent_updates">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.BiometricUnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Base" />
|
android:theme="@style/Theme.Base" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
@@ -189,10 +189,6 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- EH -->
|
<!-- EH -->
|
||||||
<service
|
|
||||||
android:name="exh.md.similar.SimilarUpdateService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="exh.eh.EHentaiUpdateWorker"
|
android:name="exh.eh.EHentaiUpdateWorker"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.multidex.MultiDex
|
import androidx.multidex.MultiDex
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.ImageLoaderFactory
|
||||||
|
import coil.decode.GifDecoder
|
||||||
|
import coil.decode.ImageDecoderDecoder
|
||||||
import com.elvishew.xlog.LogConfiguration
|
import com.elvishew.xlog.LogConfiguration
|
||||||
import com.elvishew.xlog.LogLevel
|
import com.elvishew.xlog.LogLevel
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
@@ -26,10 +39,14 @@ import com.google.firebase.analytics.ktx.analytics
|
|||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.ms_square.debugoverlay.DebugOverlay
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
|
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
@@ -40,9 +57,12 @@ import exh.log.xLogD
|
|||||||
import exh.log.xLogE
|
import exh.log.xLogE
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
@@ -53,10 +73,12 @@ import javax.net.ssl.SSLContext
|
|||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlin.time.days
|
import kotlin.time.days
|
||||||
|
|
||||||
open class App : Application(), LifecycleObserver {
|
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
@@ -71,6 +93,12 @@ open class App : Application(), LifecycleObserver {
|
|||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid potential crashes
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val process = getProcessName()
|
||||||
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||||
|
}
|
||||||
|
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
@@ -82,6 +110,34 @@ open class App : Application(), LifecycleObserver {
|
|||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
|
// Show notification to disable Incognito Mode when it's enabled
|
||||||
|
preferences.incognitoMode().asFlow()
|
||||||
|
.onEach { enabled ->
|
||||||
|
val notificationManager = NotificationManagerCompat.from(this)
|
||||||
|
if (enabled) {
|
||||||
|
disableIncognitoReceiver.register()
|
||||||
|
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||||
|
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||||
|
setContentText(getString(R.string.notification_incognito_text))
|
||||||
|
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
||||||
|
setOngoing(true)
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
this@App,
|
||||||
|
0,
|
||||||
|
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||||
|
PendingIntent.FLAG_ONE_SHOT
|
||||||
|
)
|
||||||
|
setContentIntent(pendingIntent)
|
||||||
|
}
|
||||||
|
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
||||||
|
} else {
|
||||||
|
disableIncognitoReceiver.unregister()
|
||||||
|
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
@@ -94,6 +150,23 @@ open class App : Application(), LifecycleObserver {
|
|||||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
return ImageLoader.Builder(this).apply {
|
||||||
|
componentRegistry {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
add(ImageDecoderDecoder(this@App))
|
||||||
|
} else {
|
||||||
|
add(GifDecoder())
|
||||||
|
}
|
||||||
|
add(ByteBufferFetcher())
|
||||||
|
add(MangaCoverFetcher())
|
||||||
|
}
|
||||||
|
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||||
|
crossfade(300)
|
||||||
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun workaroundAndroid7BrokenSSL() {
|
private fun workaroundAndroid7BrokenSSL() {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
||||||
@@ -215,4 +288,30 @@ open class App : Application(), LifecycleObserver {
|
|||||||
xLogE("Failed to initialize debug overlay, app in background?", e)
|
xLogE("Failed to initialize debug overlay, app in background?", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
||||||
|
private var registered = false
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
preferences.incognitoMode().set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register() {
|
||||||
|
if (!registered) {
|
||||||
|
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
||||||
|
registered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister() {
|
||||||
|
if (registered) {
|
||||||
|
unregisterReceiver(this)
|
||||||
|
registered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -44,8 +43,6 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
@@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
|||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -141,6 +143,50 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 59) {
|
||||||
|
// Reset rotation to Free after replacing Lock
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
if (prefs.contains("pref_rotation_type_key")) {
|
||||||
|
prefs.edit {
|
||||||
|
putInt("pref_rotation_type_key", 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable update check for Android 5.x users
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
|
UpdaterJob.cancelTask(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 60) {
|
||||||
|
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||||
|
1 -> OrientationType.FREE.flagValue
|
||||||
|
2 -> OrientationType.PORTRAIT.flagValue
|
||||||
|
3 -> OrientationType.LANDSCAPE.flagValue
|
||||||
|
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
|
||||||
|
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
|
||||||
|
else -> OrientationType.FREE.flagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading mode flag and prefValue is the same value
|
||||||
|
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putInt("pref_default_orientation_type_key", newOrientation)
|
||||||
|
remove("pref_rotation_type_key")
|
||||||
|
putInt("pref_default_reading_mode_key", newReadingMode)
|
||||||
|
remove("pref_default_viewer_key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 61) {
|
||||||
|
// Handle removed every 1 or 2 hour library updates
|
||||||
|
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||||
|
if (updateInterval == 1 || updateInterval == 2) {
|
||||||
|
preferences.libraryUpdateInterval().set(3)
|
||||||
|
LibraryUpdateJob.setupTask(context, 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ object BackupConst {
|
|||||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||||
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
|
||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
const val BACKUP_TYPE_FULL = 1
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
@@ -34,7 +33,9 @@ class BackupCreateService : Service() {
|
|||||||
// SY -->
|
// SY -->
|
||||||
internal const val BACKUP_CUSTOM_INFO = 0x10
|
internal const val BACKUP_CUSTOM_INFO = 0x10
|
||||||
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
||||||
internal const val BACKUP_ALL = 0x1F
|
internal const val BACKUP_READ_MANGA = 0x20
|
||||||
|
internal const val BACKUP_READ_MANGA_MASK = 0x20
|
||||||
|
internal const val BACKUP_ALL = 0x3F
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,12 +54,11 @@ class BackupCreateService : Service() {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
fun start(context: Context, uri: Uri, flags: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
putExtra(BackupConst.EXTRA_TYPE, type)
|
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@@ -106,17 +106,11 @@ class BackupCreateService : Service() {
|
|||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val backupManager = when (backupType) {
|
|
||||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
|
|
||||||
else -> LegacyBackupManager(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
notifier.showBackupComplete(unifile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifier.showBackupError(e.message)
|
notifier.showBackupError(e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -23,9 +22,6 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
if (preferences.createLegacyBackup().get()) {
|
|
||||||
LegacyBackupManager(context).createBackup(uri, flags, true)
|
|
||||||
}
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure()
|
Result.failure()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
fun showBackupComplete(unifile: UniFile) {
|
||||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
@@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUST
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||||
@@ -64,7 +66,11 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
val databaseManga = getFavoriteManga() /* SY --> */ + getReadManga() + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) {
|
||||||
|
getReadManga()
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
} + getMergedManga() /* SY <-- */
|
||||||
|
|
||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ data class BackupManga(
|
|||||||
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||||
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||||
@ProtoNumber(13) var dateAdded: Long = 0,
|
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||||
@ProtoNumber(14) var viewer: Int = 0,
|
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
||||||
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||||
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||||
@@ -35,6 +35,8 @@ data class BackupManga(
|
|||||||
@ProtoNumber(100) var favorite: Boolean = true,
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||||
|
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||||
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
|
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
|
||||||
@@ -45,7 +47,10 @@ data class BackupManga(
|
|||||||
@ProtoNumber(801) var customArtist: String? = null,
|
@ProtoNumber(801) var customArtist: String? = null,
|
||||||
@ProtoNumber(802) var customAuthor: String? = null,
|
@ProtoNumber(802) var customAuthor: String? = null,
|
||||||
@ProtoNumber(803) var customDescription: String? = null,
|
@ProtoNumber(803) var customDescription: String? = null,
|
||||||
@ProtoNumber(803) var customGenre: List<String>? = null
|
@ProtoNumber(803) var customGenre: List<String>? = null,
|
||||||
|
|
||||||
|
// Neko specific values
|
||||||
|
@ProtoNumber(901) var filtered_scanlators: String? = null,
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): MangaImpl {
|
fun getMangaImpl(): MangaImpl {
|
||||||
return MangaImpl().apply {
|
return MangaImpl().apply {
|
||||||
@@ -60,8 +65,9 @@ data class BackupManga(
|
|||||||
favorite = this@BackupManga.favorite
|
favorite = this@BackupManga.favorite
|
||||||
source = this@BackupManga.source
|
source = this@BackupManga.source
|
||||||
date_added = this@BackupManga.dateAdded
|
date_added = this@BackupManga.dateAdded
|
||||||
viewer = this@BackupManga.viewer
|
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
||||||
chapter_flags = this@BackupManga.chapterFlags
|
chapter_flags = this@BackupManga.chapterFlags
|
||||||
|
filtered_scanlators = this@BackupManga.filtered_scanlators
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +122,10 @@ data class BackupManga(
|
|||||||
favorite = manga.favorite,
|
favorite = manga.favorite,
|
||||||
source = manga.source,
|
source = manga.source,
|
||||||
dateAdded = manga.date_added,
|
dateAdded = manga.date_added,
|
||||||
viewer = manga.viewer,
|
viewer = manga.readingModeType,
|
||||||
chapterFlags = manga.chapter_flags
|
viewer_flags = manga.viewer_flags,
|
||||||
|
chapterFlags = manga.chapter_flags,
|
||||||
|
filtered_scanlators = manga.filtered_scanlators
|
||||||
// SY -->
|
// SY -->
|
||||||
).also { backupManga ->
|
).also { backupManga ->
|
||||||
customMangaManager?.getManga(manga)?.let {
|
customMangaManager?.getManga(manga)?.let {
|
||||||
|
|||||||
@@ -5,33 +5,13 @@ import android.net.Uri
|
|||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||||
import com.github.salomonbrys.kotson.set
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MERGEDMANGAREFERENCES
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.SAVEDSEARCHES
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||||
@@ -49,8 +29,6 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
@@ -62,8 +40,6 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.lang.RuntimeException
|
import java.lang.RuntimeException
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@@ -89,180 +65,8 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
||||||
// Create root object
|
throw IllegalStateException("Legacy backup creation is not supported")
|
||||||
val root = JsonObject()
|
|
||||||
|
|
||||||
// Create manga array
|
|
||||||
val mangaEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create category array
|
|
||||||
val categoryEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create extension ID/name mapping
|
|
||||||
val extensionEntries = JsonArray()
|
|
||||||
|
|
||||||
// Merged Manga References
|
|
||||||
val mergedMangaReferenceEntries = JsonArray()
|
|
||||||
|
|
||||||
// Add value's to root
|
|
||||||
root[Backup.VERSION] = CURRENT_VERSION
|
|
||||||
root[Backup.MANGAS] = mangaEntries
|
|
||||||
root[CATEGORIES] = categoryEntries
|
|
||||||
root[EXTENSIONS] = extensionEntries
|
|
||||||
// SY -->
|
|
||||||
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
|
||||||
val mangas = getFavoriteManga()/* SY --> */.filterNot { it.source == MERGED_SOURCE_ID } + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
|
||||||
|
|
||||||
// Backup library manga and its dependencies
|
|
||||||
mangas.forEach { manga ->
|
|
||||||
mangaEntries.add(backupMangaObject(manga, flags))
|
|
||||||
|
|
||||||
// Maintain set of extensions/sources used (excludes local source)
|
|
||||||
if (manga.source != LocalSource.ID) {
|
|
||||||
sourceManager.get(manga.source)?.let {
|
|
||||||
extensions.add("${manga.source}:${it.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
|
||||||
backupCategories(categoryEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup extension ID/name mapping
|
|
||||||
backupExtensionInfo(extensionEntries, extensions)
|
|
||||||
// SY -->
|
|
||||||
root[SAVEDSEARCHES] =
|
|
||||||
Injekt.get<PreferencesHelper>().savedSearches().get().joinToString(separator = "***")
|
|
||||||
|
|
||||||
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val file: UniFile = (
|
|
||||||
if (isJob) {
|
|
||||||
// Get dir of file and create
|
|
||||||
var dir = UniFile.fromUri(context, uri)
|
|
||||||
dir = dir.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = numberOfBackups()
|
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
|
||||||
.sortedByDescending { it.name }
|
|
||||||
.drop(numberOfBackups - 1)
|
|
||||||
.forEach { it.delete() }
|
|
||||||
|
|
||||||
// Create new file to place backup
|
|
||||||
dir.createFile(Backup.getDefaultFilename())
|
|
||||||
} else {
|
|
||||||
UniFile.fromUri(context, uri)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
|
|
||||||
file.openOutputStream().bufferedWriter().use {
|
|
||||||
parser.toJson(root, it)
|
|
||||||
}
|
|
||||||
return file.uri.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
|
||||||
extensions.sorted().forEach {
|
|
||||||
root.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
private fun backupMergedMangaReferences(root: JsonArray) {
|
|
||||||
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
|
||||||
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup the categories of library
|
|
||||||
*
|
|
||||||
* @param root root of categories json
|
|
||||||
*/
|
|
||||||
internal fun backupCategories(root: JsonArray) {
|
|
||||||
val categories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a manga to Json
|
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @return [JsonElement] containing manga information
|
|
||||||
*/
|
|
||||||
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
|
||||||
// Entry for this manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
|
|
||||||
// Backup manga fields
|
|
||||||
entry[MANGA] = parser.toJsonTree(manga)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
|
||||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
val chaptersJson = parser.toJsonTree(chapters)
|
|
||||||
if (chaptersJson.asJsonArray.size() > 0) {
|
|
||||||
entry[CHAPTERS] = chaptersJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
val categoriesNames = categoriesForManga.map { it.name }
|
|
||||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
|
||||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
entry[TRACK] = parser.toJsonTree(tracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
|
||||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
|
||||||
if (historyForManga.isNotEmpty()) {
|
|
||||||
val historyData = historyForManga.mapNotNull { history ->
|
|
||||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
|
||||||
url?.let { DHistory(url, history.last_read) }
|
|
||||||
}
|
|
||||||
val historyJson = parser.toJsonTree(historyData)
|
|
||||||
if (historyJson.asJsonArray.size() > 0) {
|
|
||||||
entry[HISTORY] = historyJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
manga.id = dbManga.id
|
manga.id = dbManga.id
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ object MangaTypeAdapter {
|
|||||||
value(it.originalTitle)
|
value(it.originalTitle)
|
||||||
// SY <--
|
// SY <--
|
||||||
value(it.source)
|
value(it.source)
|
||||||
value(it.viewer)
|
value(it.viewer_flags)
|
||||||
value(it.chapter_flags)
|
value(it.chapter_flags)
|
||||||
endArray()
|
endArray()
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ object MangaTypeAdapter {
|
|||||||
manga.url = nextString()
|
manga.url = nextString()
|
||||||
manga.title = nextString()
|
manga.title = nextString()
|
||||||
manga.source = nextLong()
|
manga.source = nextLong()
|
||||||
manga.viewer = nextInt()
|
manga.viewer_flags = nextInt()
|
||||||
manga.chapter_flags = nextInt()
|
manga.chapter_flags = nextInt()
|
||||||
endArray()
|
endArray()
|
||||||
manga
|
manga
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import coil.imageLoader
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -99,6 +100,13 @@ class CoverCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear coil's memory cache.
|
||||||
|
*/
|
||||||
|
fun clearMemoryCache() {
|
||||||
|
context.imageLoader.memoryCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCacheDir(dir: String): File {
|
private fun getCacheDir(dir: String): File {
|
||||||
return context.getExternalFilesDir(dir)
|
return context.getExternalFilesDir(dir)
|
||||||
?: File(context.filesDir, dir).also { it.mkdirs() }
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DataSource
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.fetch.FetchResult
|
||||||
|
import coil.fetch.Fetcher
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.size.Size
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
||||||
|
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = ByteArrayInputStream(data.array()).source().buffer(),
|
||||||
|
mimeType = null,
|
||||||
|
dataSource = DataSource.MEMORY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun key(data: ByteBuffer): String? = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DataSource
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.fetch.FetchResult
|
||||||
|
import coil.fetch.Fetcher
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.network.HttpException
|
||||||
|
import coil.request.get
|
||||||
|
import coil.size.Size
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||||
|
*
|
||||||
|
* Available request parameter:
|
||||||
|
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||||
|
*/
|
||||||
|
class MangaCoverFetcher : Fetcher<Manga> {
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
||||||
|
|
||||||
|
override fun key(data: Manga): String? {
|
||||||
|
if (data.thumbnail_url.isNullOrBlank()) return null
|
||||||
|
return data.thumbnail_url!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
||||||
|
// Use custom cover if exists
|
||||||
|
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
||||||
|
val customCoverFile = coverCache.getCustomCoverFile(data)
|
||||||
|
if (useCustomCover && customCoverFile.exists()) {
|
||||||
|
return fileLoader(customCoverFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cover = data.thumbnail_url
|
||||||
|
return when (getResourceType(cover)) {
|
||||||
|
Type.URL -> httpLoader(data, options)
|
||||||
|
Type.File -> fileLoader(data)
|
||||||
|
null -> error("Invalid image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||||
|
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
|
|
||||||
|
// Use previously cached cover if exist
|
||||||
|
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
||||||
|
if (!manga.favorite) {
|
||||||
|
coverFile.setLastModified(Date().time)
|
||||||
|
}
|
||||||
|
return fileLoader(coverFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (response, body) = awaitGetCall(manga, options)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
body.close()
|
||||||
|
throw HttpException(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to disk for future use
|
||||||
|
if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
|
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
||||||
|
tmpFile.parentFile?.mkdirs()
|
||||||
|
tmpFile.sink().buffer().use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
if (coverFile.exists()) {
|
||||||
|
coverFile.delete()
|
||||||
|
}
|
||||||
|
tmpFile.renameTo(coverFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceResult(
|
||||||
|
source = body.source(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
||||||
|
val call = getCall(manga, options)
|
||||||
|
val response = call.await()
|
||||||
|
return response to checkNotNull(response.body) { "Null response source" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCall(manga: Manga, options: Options): Call {
|
||||||
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
|
val client = source?.client ?: defaultClient
|
||||||
|
|
||||||
|
val newClient = client.newBuilder().build()
|
||||||
|
|
||||||
|
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||||
|
if (source != null) {
|
||||||
|
it.headers(source.headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
val networkRead = options.networkCachePolicy.readEnabled
|
||||||
|
val diskRead = options.diskCachePolicy.readEnabled
|
||||||
|
when {
|
||||||
|
!networkRead && diskRead -> {
|
||||||
|
it.cacheControl(CacheControl.FORCE_CACHE)
|
||||||
|
}
|
||||||
|
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
it.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
|
} else {
|
||||||
|
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||||
|
}
|
||||||
|
!networkRead && !diskRead -> {
|
||||||
|
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||||
|
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return newClient.newCall(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileLoader(manga: Manga): FetchResult {
|
||||||
|
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileLoader(file: File): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = file.source().buffer(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getResourceType(cover: String?): Type? {
|
||||||
|
return when {
|
||||||
|
cover.isNullOrEmpty() -> null
|
||||||
|
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
||||||
|
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class Type {
|
||||||
|
File, URL
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||||
|
|
||||||
|
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
||||||
|
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,9 +21,6 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
import exh.md.similar.sql.mappers.SimilarTypeMapping
|
|
||||||
import exh.md.similar.sql.models.MangaSimilar
|
|
||||||
import exh.md.similar.sql.queries.SimilarQueries
|
|
||||||
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.merged.sql.queries.MergedQueries
|
import exh.merged.sql.queries.MergedQueries
|
||||||
@@ -42,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 /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
|
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)
|
||||||
@@ -62,7 +59,6 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
.addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
|
|
||||||
// SY <--
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
import exh.md.similar.sql.tables.SimilarTable
|
|
||||||
import exh.merged.sql.tables.MergedTable
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
@@ -25,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
|
const val DATABASE_VERSION = /* SY --> */ 7 /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -40,7 +39,6 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTagTable.createTableQuery)
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
execSQL(SimilarTable.createTableQuery)
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
@@ -57,7 +55,6 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
execSQL(MergedTable.createIndexQuery)
|
execSQL(MergedTable.createIndexQuery)
|
||||||
execSQL(SimilarTable.createMangaIdIndexQuery)
|
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +71,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MergedTable.createTableQuery)
|
db.execSQL(MergedTable.createTableQuery)
|
||||||
db.execSQL(MergedTable.createIndexQuery)
|
db.execSQL(MergedTable.createIndexQuery)
|
||||||
}
|
}
|
||||||
if (oldVersion < 5) {
|
/*if (oldVersion < 5) {
|
||||||
db.execSQL(SimilarTable.createTableQuery)
|
db.execSQL(SimilarTable.createTableQuery)
|
||||||
db.execSQL(SimilarTable.createMangaIdIndexQuery)
|
db.execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||||
|
}*/
|
||||||
|
if (oldVersion < 6) {
|
||||||
|
db.execSQL(MangaTable.addFilteredScanlators)
|
||||||
|
}
|
||||||
|
if (oldVersion < 7) {
|
||||||
|
db.execSQL("DROP TABLE IF EXISTS manga_related")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
@@ -65,10 +66,11 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer,
|
COL_VIEWER to obj.viewer_flags,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||||
COL_DATE_ADDED to obj.date_added
|
COL_DATE_ADDED to obj.date_added,
|
||||||
|
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +89,11 @@ interface BaseMangaGetResolver {
|
|||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
|
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
interface Manga : SManga {
|
interface Manga : SManga {
|
||||||
@@ -15,18 +17,20 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer: Int
|
var viewer_flags: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
var cover_last_modified: Long
|
var cover_last_modified: Long
|
||||||
|
|
||||||
|
var filtered_scanlators: String?
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setChapterFlags(order, CHAPTER_SORT_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
@@ -39,60 +43,72 @@ interface Manga : SManga {
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setViewerFlags(flag: Int, mask: Int) {
|
||||||
|
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
var displayMode: Int
|
var displayMode: Int
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
get() = chapter_flags and CHAPTER_DISPLAY_MASK
|
||||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
|
||||||
|
|
||||||
var readFilter: Int
|
var readFilter: Int
|
||||||
get() = chapter_flags and READ_MASK
|
get() = chapter_flags and CHAPTER_READ_MASK
|
||||||
set(filter) = setFlags(filter, READ_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
|
||||||
|
|
||||||
var downloadedFilter: Int
|
var downloadedFilter: Int
|
||||||
get() = chapter_flags and DOWNLOADED_MASK
|
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
|
||||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
|
||||||
|
|
||||||
var bookmarkedFilter: Int
|
var bookmarkedFilter: Int
|
||||||
get() = chapter_flags and BOOKMARKED_MASK
|
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
|
||||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
|
||||||
|
|
||||||
var sorting: Int
|
var sorting: Int
|
||||||
get() = chapter_flags and SORTING_MASK
|
get() = chapter_flags and CHAPTER_SORTING_MASK
|
||||||
set(sort) = setFlags(sort, SORTING_MASK)
|
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
|
||||||
|
|
||||||
|
var readingModeType: Int
|
||||||
|
get() = viewer_flags and ReadingModeType.MASK
|
||||||
|
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
|
||||||
|
|
||||||
|
var orientationType: Int
|
||||||
|
get() = viewer_flags and OrientationType.MASK
|
||||||
|
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val SORT_DESC = 0x00000000
|
|
||||||
const val SORT_ASC = 0x00000001
|
|
||||||
const val SORT_MASK = 0x00000001
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
// Generic filter that does not filter anything
|
||||||
const val SHOW_ALL = 0x00000000
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
const val SHOW_UNREAD = 0x00000002
|
const val CHAPTER_SORT_DESC = 0x00000000
|
||||||
const val SHOW_READ = 0x00000004
|
const val CHAPTER_SORT_ASC = 0x00000001
|
||||||
const val READ_MASK = 0x00000006
|
const val CHAPTER_SORT_MASK = 0x00000001
|
||||||
|
|
||||||
const val SHOW_DOWNLOADED = 0x00000008
|
const val CHAPTER_SHOW_UNREAD = 0x00000002
|
||||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
const val CHAPTER_SHOW_READ = 0x00000004
|
||||||
const val DOWNLOADED_MASK = 0x00000018
|
const val CHAPTER_READ_MASK = 0x00000006
|
||||||
|
|
||||||
const val SHOW_BOOKMARKED = 0x00000020
|
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
|
||||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
|
||||||
const val BOOKMARKED_MASK = 0x00000060
|
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
|
||||||
|
|
||||||
const val SORTING_SOURCE = 0x00000000
|
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
|
||||||
const val SORTING_NUMBER = 0x00000100
|
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
|
||||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
|
||||||
const val SORTING_MASK = 0x00000300
|
|
||||||
|
|
||||||
const val DISPLAY_NAME = 0x00000000
|
const val CHAPTER_SORTING_SOURCE = 0x00000000
|
||||||
const val DISPLAY_NUMBER = 0x00100000
|
const val CHAPTER_SORTING_NUMBER = 0x00000100
|
||||||
const val DISPLAY_MASK = 0x00100000
|
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
|
||||||
|
const val CHAPTER_SORTING_MASK = 0x00000300
|
||||||
|
|
||||||
|
const val CHAPTER_DISPLAY_NAME = 0x00000000
|
||||||
|
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
||||||
|
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
this.source = source
|
this.source = source
|
||||||
|
|||||||
@@ -56,12 +56,14 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var viewer: Int = 0
|
override var viewer_flags: Int = 0
|
||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
override var cover_last_modified: Long = 0
|
override var cover_last_modified: Long = 0
|
||||||
|
|
||||||
|
override var filtered_scanlators: String? = null
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
lateinit var ogTitle: String
|
lateinit var ogTitle: String
|
||||||
private set
|
private set
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@@ -114,14 +114,24 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
fun updateFlags(manga: Manga) = db.put()
|
fun updateChapterFlags(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver())
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateFlags(mangas: List<Manga>) = db.put()
|
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||||
.objects(mangas)
|
.objects(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateViewerFlags(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||||
|
.objects(manga)
|
||||||
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
@@ -134,11 +144,6 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFavoritePutResolver())
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaViewer(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaViewerPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateMangaTitle(manga: Manga) = db.put()
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
@@ -149,6 +154,13 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaFilteredScanlatorsPutResolver())
|
||||||
|
.prepare()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|||||||
+7
-7
@@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class MangaViewerPutResolver : PutResolver<Manga>() {
|
// [EXH]
|
||||||
|
class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@@ -21,12 +22,11 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_FILTERED_SCANLATORS} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.filtered_scanlators)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
contentValuesOf(
|
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators
|
||||||
MangaTable.COL_VIEWER to manga.viewer
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
+3
-2
@@ -8,8 +8,9 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@@ -37,6 +38,6 @@ class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolve
|
|||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
colName to fieldGetter.get(manga)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -30,6 +30,6 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
|
|||||||
MangaTable.COL_DATE_ADDED to manga.date_added,
|
MangaTable.COL_DATE_ADDED to manga.date_added,
|
||||||
MangaTable.COL_TITLE to manga.title,
|
MangaTable.COL_TITLE to manga.title,
|
||||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
||||||
MangaTable.COL_VIEWER to manga.viewer
|
MangaTable.COL_VIEWER to manga.viewer_flags
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ object MangaTable {
|
|||||||
|
|
||||||
// SY ->>
|
// SY ->>
|
||||||
const val COL_READ = "read"
|
const val COL_READ = "read"
|
||||||
|
|
||||||
|
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
@@ -65,7 +67,8 @@ object MangaTable {
|
|||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
||||||
$COL_DATE_ADDED LONG NOT NULL
|
$COL_DATE_ADDED LONG NOT NULL,
|
||||||
|
$COL_FILTERED_SCANLATORS TEXT
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
@@ -90,4 +93,7 @@ object MangaTable {
|
|||||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
|
|
||||||
|
val addFilteredScanlators: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,15 @@ class DownloadManager(private val context: Context) {
|
|||||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deletePendingDownloads(vararg downloads: Download) {
|
||||||
|
val downloadsByManga = downloads.groupBy { it.manga.id }
|
||||||
|
downloadsByManga.map { entry ->
|
||||||
|
val manga = entry.value.first().manga
|
||||||
|
val source = entry.value.first().source
|
||||||
|
deleteChapters(entry.value.map { it.chapter }, manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the directories of a list of downloaded chapters.
|
* Deletes the directories of a list of downloaded chapters.
|
||||||
*
|
*
|
||||||
@@ -263,7 +272,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
|
|
||||||
if (removeNonFavorite && !manga.favorite) {
|
if (removeNonFavorite && !manga.favorite) {
|
||||||
val mangaFolder = provider.getMangaDir(manga, source)
|
val mangaFolder = provider.getMangaDir(manga, source)
|
||||||
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
|
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
|
||||||
mangaFolder.delete()
|
mangaFolder.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
return cleaned
|
return cleaned
|
||||||
@@ -284,8 +293,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
|
|
||||||
if (cache.getDownloadCount(manga) == 0) {
|
if (cache.getDownloadCount(manga) == 0) {
|
||||||
val mangaFolder = provider.getMangaDir(manga, source)
|
val mangaFolder = provider.getMangaDir(manga, source)
|
||||||
val size = mangaFolder.listFiles()?.size ?: 0
|
if (!mangaFolder.listFiles().isNullOrEmpty()) {
|
||||||
if (size == 0) {
|
|
||||||
mangaFolder.delete()
|
mangaFolder.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
||||||
val sourceDir = findSourceDir(source)
|
val sourceDir = findSourceDir(source)
|
||||||
return sourceDir?.findFile(getMangaDirName(manga))
|
return sourceDir?.findFile(getMangaDirName(manga), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
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) ?: mangaDir?.findFile("$it.cbz") }
|
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,14 +123,12 @@ class DownloadProvider(private val context: Context) {
|
|||||||
source: Source
|
source: Source
|
||||||
): List<UniFile> {
|
): List<UniFile> {
|
||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return mangaDir.listFiles()!!.asList().filter {
|
return mangaDir.listFiles().orEmpty().asList().filter {
|
||||||
(
|
chapters.find { chp ->
|
||||||
chapters.find { chp ->
|
getValidChapterDirNames(chp).any { dir ->
|
||||||
getValidChapterDirNames(chp).any { dir ->
|
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
||||||
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
}
|
||||||
}
|
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||||
} == null
|
|
||||||
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -141,7 +139,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun getSourceDirName(source: Source): String {
|
fun getSourceDirName(source: Source): String {
|
||||||
return source.toString()
|
return DiskUtil.buildValidFilename(source.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,6 +176,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return listOf(
|
return listOf(
|
||||||
getChapterDirName(chapter),
|
getChapterDirName(chapter),
|
||||||
|
|
||||||
|
// TODO: remove this
|
||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.ContentValues.TAG
|
|
||||||
import android.util.Log
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
|
||||||
|
|
||||||
private var data: InputStream? = null
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
loadFromFile(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
loadFromFile(File(filePath), callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
try {
|
|
||||||
data = FileInputStream(file)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
||||||
Timber.d(e, "Failed to open file")
|
|
||||||
}
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onDataReady(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
try {
|
|
||||||
data?.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Ignored.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataClass(): Class<InputStream> {
|
|
||||||
return InputStream::class.java
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataSource(): DataSource {
|
|
||||||
return DataSource.LOCAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
open class LibraryMangaCustomCoverFetcher(
|
|
||||||
private val manga: Manga,
|
|
||||||
private val coverCache: CoverCache
|
|
||||||
) : FileFetcher() {
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
getCustomCoverFile()?.let {
|
|
||||||
loadFromFile(it, callback)
|
|
||||||
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun getCustomCoverFile(): File? {
|
|
||||||
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [DataFetcher] for loading a cover of a library manga.
|
|
||||||
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
|
||||||
* and copies the result to the cache.
|
|
||||||
*
|
|
||||||
* @param networkFetcher the network fetcher for this cover.
|
|
||||||
* @param manga the manga of the cover to load.
|
|
||||||
* @param file the file where this cover should be. It may exists or not.
|
|
||||||
*/
|
|
||||||
class LibraryMangaUrlFetcher(
|
|
||||||
private val networkFetcher: DataFetcher<InputStream>,
|
|
||||||
private val manga: Manga,
|
|
||||||
private val coverCache: CoverCache
|
|
||||||
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
getCustomCoverFile()?.let {
|
|
||||||
loadFromFile(it, callback)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val cover = coverCache.getCoverFile(manga)
|
|
||||||
if (cover == null) {
|
|
||||||
callback.onLoadFailed(Exception("Null thumbnail url"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cover.exists()) {
|
|
||||||
networkFetcher.loadData(
|
|
||||||
priority,
|
|
||||||
object : DataFetcher.DataCallback<InputStream> {
|
|
||||||
override fun onDataReady(data: InputStream?) {
|
|
||||||
if (data != null) {
|
|
||||||
val tmpFile = File(cover.path + ".tmp")
|
|
||||||
try {
|
|
||||||
// Retrieve destination stream, create parent folders if needed.
|
|
||||||
val output = try {
|
|
||||||
tmpFile.outputStream()
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
tmpFile.parentFile!!.mkdirs()
|
|
||||||
tmpFile.outputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the file and rename to the original.
|
|
||||||
data.use { output.use { data.copyTo(output) } }
|
|
||||||
tmpFile.renameTo(cover)
|
|
||||||
loadFromFile(cover, callback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
tmpFile.delete()
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback.onLoadFailed(Exception("Null data"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadFailed(e: Exception) {
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
loadFromFile(cover, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
super.cleanup()
|
|
||||||
networkFetcher.cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
super.cancel()
|
|
||||||
networkFetcher.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.load.Key
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
|
||||||
val key = manga.url + coverLastModified
|
|
||||||
|
|
||||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
|
||||||
messageDigest.update(key.toByteArray(Key.CHARSET))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
|
||||||
import com.bumptech.glide.load.Options
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import com.bumptech.glide.load.model.Headers
|
|
||||||
import com.bumptech.glide.load.model.LazyHeaders
|
|
||||||
import com.bumptech.glide.load.model.ModelLoader
|
|
||||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
|
||||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
|
||||||
*
|
|
||||||
* - Check in RAM LRU.
|
|
||||||
* - Check in disk LRU.
|
|
||||||
* - Check in this module.
|
|
||||||
* - Fetch from the network connection.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cover cache where persistent covers are stored.
|
|
||||||
*/
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default network client.
|
|
||||||
*/
|
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map where request headers are stored for a source.
|
|
||||||
*/
|
|
||||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class for creating [MangaThumbnailModelLoader] instances.
|
|
||||||
*/
|
|
||||||
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
|
|
||||||
|
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
|
|
||||||
return MangaThumbnailModelLoader()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun teardown() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(model: MangaThumbnail): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a fetcher for the given manga or null if the url is empty.
|
|
||||||
*
|
|
||||||
* @param mangaThumbnail the model.
|
|
||||||
* @param width the width of the view where the resource will be loaded.
|
|
||||||
* @param height the height of the view where the resource will be loaded.
|
|
||||||
*/
|
|
||||||
override fun buildLoadData(
|
|
||||||
mangaThumbnail: MangaThumbnail,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
options: Options
|
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
|
||||||
val manga = mangaThumbnail.manga
|
|
||||||
val url = manga.thumbnail_url
|
|
||||||
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
return if (!manga.favorite || manga.isLocal()) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith("http", true)) {
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
|
||||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
|
||||||
|
|
||||||
// Get the resource fetcher for this request url.
|
|
||||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
|
||||||
|
|
||||||
if (!manga.favorite) {
|
|
||||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
|
||||||
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
|
||||||
} else {
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
|
||||||
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
|
||||||
*
|
|
||||||
* @param manga the model.
|
|
||||||
*/
|
|
||||||
private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
|
||||||
if (source == null) return LazyHeaders.DEFAULT
|
|
||||||
|
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
|
||||||
LazyHeaders.Builder().apply {
|
|
||||||
val nullStr: String? = null
|
|
||||||
setHeader("User-Agent", nullStr)
|
|
||||||
for ((key, value) in source.headers.toMultimap()) {
|
|
||||||
addHeader(key, value[0])
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.Options
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import com.bumptech.glide.load.model.ModelLoader
|
|
||||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
|
||||||
|
|
||||||
override fun buildLoadData(
|
|
||||||
model: InputStream,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
options: Options
|
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
|
||||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(model: InputStream): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
|
|
||||||
|
|
||||||
override fun getDataClass(): Class<InputStream> {
|
|
||||||
return InputStream::class.java
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
try {
|
|
||||||
stream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataSource(): DataSource {
|
|
||||||
return DataSource.LOCAL
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadData(
|
|
||||||
priority: Priority,
|
|
||||||
callback: DataFetcher.DataCallback<in InputStream>
|
|
||||||
) {
|
|
||||||
callback.onDataReady(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class for creating [PassthroughModelLoader] instances.
|
|
||||||
*/
|
|
||||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
|
||||||
|
|
||||||
override fun build(
|
|
||||||
multiFactory: MultiModelLoaderFactory
|
|
||||||
): ModelLoader<InputStream, InputStream> {
|
|
||||||
return PassthroughModelLoader()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun teardown() {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.GlideBuilder
|
|
||||||
import com.bumptech.glide.Registry
|
|
||||||
import com.bumptech.glide.annotation.GlideModule
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
|
||||||
import com.bumptech.glide.module.AppGlideModule
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to update Glide module settings
|
|
||||||
*/
|
|
||||||
@GlideModule
|
|
||||||
class TachiGlideModule : AppGlideModule() {
|
|
||||||
|
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
|
||||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
|
||||||
builder.setDefaultTransitionOptions(
|
|
||||||
Drawable::class.java,
|
|
||||||
DrawableTransitionOptions.withCrossFade()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
||||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
|
||||||
|
|
||||||
registry.replace(
|
|
||||||
GlideUrl::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
networkFactory
|
|
||||||
)
|
|
||||||
registry.append(
|
|
||||||
MangaThumbnail::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
MangaThumbnailModelLoader.Factory()
|
|
||||||
)
|
|
||||||
registry.append(
|
|
||||||
InputStream::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
PassthroughModelLoader.Factory()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,9 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import eu.kanade.tachiyomi.data.preference.CHARGING
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -31,9 +33,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateRestriction()!!
|
val restrictions = preferences.libraryUpdateRestriction().get()
|
||||||
val acRestriction = "ac" in restrictions
|
val acRestriction = CHARGING in restrictions
|
||||||
val wifiRestriction = if ("wifi" in restrictions) {
|
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
|
||||||
NetworkType.UNMETERED
|
NetworkType.UNMETERED
|
||||||
} else {
|
} else {
|
||||||
NetworkType.CONNECTED
|
NetworkType.CONNECTED
|
||||||
|
|||||||
@@ -6,20 +6,23 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.bumptech.glide.Glide
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
@@ -76,7 +79,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_PROGRESS,
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(title)
|
.setContentTitle(title.chop(40))
|
||||||
|
.setContentText("($current/$total)")
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@@ -166,14 +170,17 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Per-manga notification
|
// Per-manga notification
|
||||||
if (!preferences.hideNotificationContent()) {
|
if (!preferences.hideNotificationContent()) {
|
||||||
updates.forEach { (manga, chapters) ->
|
launchUI {
|
||||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
updates.forEach { (manga, chapters) ->
|
||||||
|
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||||
|
val icon = getMangaIcon(manga)
|
||||||
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
setContentTitle(manga.title)
|
setContentTitle(manga.title)
|
||||||
|
|
||||||
@@ -183,7 +190,6 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
val icon = getMangaIcon(manga)
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
setLargeIcon(icon)
|
setLargeIcon(icon)
|
||||||
}
|
}
|
||||||
@@ -227,23 +233,14 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMangaIcon(manga: Manga): Bitmap? {
|
private suspend fun getMangaIcon(manga: Manga): Bitmap? {
|
||||||
return try {
|
val request = ImageRequest.Builder(context)
|
||||||
Glide.with(context)
|
.data(manga)
|
||||||
.asBitmap()
|
.transformations(CircleCropTransformation())
|
||||||
.load(manga.toMangaThumbnail())
|
.size(NOTIF_ICON_SIZE)
|
||||||
.dontTransform()
|
.build()
|
||||||
.centerCrop()
|
val drawable = context.imageLoader.execute(request).drawable
|
||||||
.circleCrop()
|
return (drawable as? BitmapDrawable)?.bitmap
|
||||||
.override(
|
|
||||||
NOTIF_ICON_SIZE,
|
|
||||||
NOTIF_ICON_SIZE
|
|
||||||
)
|
|
||||||
.submit()
|
|
||||||
.get()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
||||||
|
|||||||
@@ -22,16 +22,18 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
@@ -45,6 +47,7 @@ import exh.metadata.metadata.base.insertFlatMetadataAsync
|
|||||||
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
|
import exh.source.isMdBasedSource
|
||||||
import exh.source.mangaDexSourceIds
|
import exh.source.mangaDexSourceIds
|
||||||
import exh.util.executeOnIO
|
import exh.util.executeOnIO
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
@@ -353,6 +356,7 @@ class LibraryUpdateService(
|
|||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
mangaToUpdate.groupBy { it.source }
|
mangaToUpdate.groupBy { it.source }
|
||||||
@@ -391,6 +395,10 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(manga to errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferences.autoUpdateTrackers()) {
|
||||||
|
updateTrackings(manga, loggedServices)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,7 +468,7 @@ class LibraryUpdateService(
|
|||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
}
|
}
|
||||||
ioScope.launch(handler) {
|
ioScope.launch(handler) {
|
||||||
if (source is MangaDex && trackManager.mdList.isLogged) {
|
if (source.isMdBasedSource() && trackManager.mdList.isLogged) {
|
||||||
val tracks = db.getTracks(manga).executeOnIO()
|
val tracks = db.getTracks(manga).executeOnIO()
|
||||||
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
|
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
|
||||||
var track = trackManager.mdList.createInitialTracker(manga)
|
var track = trackManager.mdList.createInitialTracker(manga)
|
||||||
@@ -507,6 +515,7 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coverCache.clearMemoryCache()
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,27 +536,35 @@ class LibraryUpdateService(
|
|||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||||
|
|
||||||
// Update the tracking details.
|
// Update the tracking details.
|
||||||
db.getTracks(manga).executeAsBlocking()
|
updateTrackings(manga, loggedServices)
|
||||||
.map { track ->
|
}
|
||||||
supervisorScope {
|
|
||||||
async {
|
notifier.cancelProgressNotification()
|
||||||
val service = trackManager.getService(track.sync_id)
|
}
|
||||||
if (service != null && service in loggedServices) {
|
|
||||||
try {
|
private suspend fun updateTrackings(manga: LibraryManga, loggedServices: List<TrackService>) {
|
||||||
val updatedTrack = service.refresh(track)
|
db.getTracks(manga).executeAsBlocking()
|
||||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
.map { track ->
|
||||||
} catch (e: Throwable) {
|
supervisorScope {
|
||||||
// Ignore errors and continue
|
async {
|
||||||
Timber.e(e)
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service in loggedServices) {
|
||||||
|
try {
|
||||||
|
val updatedTrack = service.refresh(track)
|
||||||
|
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||||
|
|
||||||
|
if (service is UnattendedTrackService) {
|
||||||
|
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignore errors and continue
|
||||||
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.awaitAll()
|
}
|
||||||
}
|
.awaitAll()
|
||||||
|
|
||||||
notifier.cancelProgressNotification()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -560,9 +577,9 @@ class LibraryUpdateService(
|
|||||||
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
||||||
|
|
||||||
val size: Int
|
val size: Int
|
||||||
mangaDex.fetchAllFollows(true)
|
mangaDex.fetchAllFollows()
|
||||||
.filter { (_, metadata) ->
|
.filter { (_, metadata) ->
|
||||||
syncFollowStatusInts.contains(metadata.follow_status)
|
syncFollowStatusInts.contains(metadata.followStatus)
|
||||||
}
|
}
|
||||||
.also { size = it.size }
|
.also { size = it.size }
|
||||||
.forEach { (networkManga, metadata) ->
|
.forEach { (networkManga, metadata) ->
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.md.similar.SimilarUpdateService
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -59,22 +58,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_SHARE_IMAGE ->
|
ACTION_SHARE_IMAGE ->
|
||||||
shareImage(
|
shareImage(
|
||||||
context,
|
context,
|
||||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Delete image from path and dismiss notification
|
// Delete image from path and dismiss notification
|
||||||
ACTION_DELETE_IMAGE ->
|
ACTION_DELETE_IMAGE ->
|
||||||
deleteImage(
|
deleteImage(
|
||||||
context,
|
context,
|
||||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Share backup file
|
// Share backup file
|
||||||
ACTION_SHARE_BACKUP ->
|
ACTION_SHARE_BACKUP ->
|
||||||
shareFile(
|
shareFile(
|
||||||
context,
|
context,
|
||||||
intent.getParcelableExtra(EXTRA_URI),
|
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||||
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
|
"application/x-protobuf+gzip",
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||||
@@ -107,13 +106,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_SHARE_CRASH_LOG ->
|
ACTION_SHARE_CRASH_LOG ->
|
||||||
shareFile(
|
shareFile(
|
||||||
context,
|
context,
|
||||||
intent.getParcelableExtra(EXTRA_URI),
|
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||||
"text/plain",
|
"text/plain",
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// SY -->
|
|
||||||
ACTION_CANCEL_SIMILAR_UPDATE -> cancelSimilarUpdate(context)
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,18 +251,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
|
||||||
/**
|
|
||||||
* Method called when user wants to stop a similar manga update
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
*/
|
|
||||||
private fun cancelSimilarUpdate(context: Context) {
|
|
||||||
SimilarUpdateService.stop(context)
|
|
||||||
Handler().post { dismissNotification(context, Notifications.ID_SIMILAR_PROGRESS) }
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@@ -297,12 +281,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||||
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
|
|
||||||
|
|
||||||
// Sy -->
|
|
||||||
// Called to cancel similar manga update.
|
|
||||||
private const val ACTION_CANCEL_SIMILAR_UPDATE = "$ID.$NAME.CANCEL_SIMILAR_UPDATE"
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||||
@@ -515,11 +493,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
|
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_SHARE_BACKUP
|
action = ACTION_SHARE_BACKUP
|
||||||
putExtra(EXTRA_URI, uri)
|
putExtra(EXTRA_URI, uri)
|
||||||
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
|
|
||||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
@@ -572,20 +549,5 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
|
||||||
/**
|
|
||||||
* Returns [PendingIntent] that starts a service which stops the similar update
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @return [PendingIntent]
|
|
||||||
*/
|
|
||||||
internal fun cancelSimilarUpdatePendingBroadcast(context: Context): PendingIntent {
|
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
|
||||||
action = ACTION_CANCEL_SIMILAR_UPDATE
|
|
||||||
}
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,14 +68,11 @@ object Notifications {
|
|||||||
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
||||||
const val ID_CRASH_LOGS = -601
|
const val ID_CRASH_LOGS = -601
|
||||||
|
|
||||||
// SY -->
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used for backup and restore.
|
* Notification channel used for Incognito Mode
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_SIMILAR = "similar_channel"
|
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
||||||
const val ID_SIMILAR_PROGRESS = -901
|
const val ID_INCOGNITO_MODE = -701
|
||||||
const val ID_SIMILAR_COMPLETE = -902
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
"downloader_channel",
|
"downloader_channel",
|
||||||
@@ -165,12 +162,10 @@ object Notifications {
|
|||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_SIMILAR,
|
CHANNEL_INCOGNITO_MODE,
|
||||||
context.getString(R.string.similar_manga),
|
context.getString(R.string.pref_incognito_mode),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
)
|
||||||
setShowBadge(false)
|
|
||||||
}
|
|
||||||
).forEach(context.notificationManager::createNotificationChannel)
|
).forEach(context.notificationManager::createNotificationChannel)
|
||||||
|
|
||||||
// Delete old notification channels
|
// Delete old notification channels
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val confirmExit = "pref_confirm_exit"
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val hideBottomBar = "pref_hide_bottom_bar_on_scroll"
|
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
||||||
|
|
||||||
const val rotation = "pref_rotation_type_key"
|
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom"
|
||||||
|
|
||||||
const val enableTransitionsPager = "pref_enable_transitions_pager_key"
|
const val enableTransitionsPager = "pref_enable_transitions_pager_key"
|
||||||
|
|
||||||
@@ -53,7 +53,11 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val colorFilterMode = "color_filter_mode"
|
const val colorFilterMode = "color_filter_mode"
|
||||||
|
|
||||||
const val defaultViewer = "pref_default_viewer_key"
|
const val grayscale = "pref_grayscale"
|
||||||
|
|
||||||
|
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||||
|
|
||||||
|
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||||
|
|
||||||
const val imageScaleType = "pref_image_scale_type_key"
|
const val imageScaleType = "pref_image_scale_type_key"
|
||||||
|
|
||||||
@@ -97,6 +101,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
|
const val autoAddTrack = "pref_auto_add_track_key"
|
||||||
|
|
||||||
const val lastUsedSource = "last_catalogue_source"
|
const val lastUsedSource = "last_catalogue_source"
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
const val lastUsedCategory = "last_used_category"
|
||||||
@@ -111,6 +117,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
|
const val folderPerManga = "create_folder_per_manga"
|
||||||
|
|
||||||
const val numberOfBackups = "backup_slots"
|
const val numberOfBackups = "backup_slots"
|
||||||
|
|
||||||
const val backupInterval = "backup_interval"
|
const val backupInterval = "backup_interval"
|
||||||
@@ -154,7 +162,7 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val useBiometricLock = "use_biometric_lock"
|
const val useAuthenticator = "use_biometric_lock"
|
||||||
|
|
||||||
const val lockAppAfter = "lock_app_after"
|
const val lockAppAfter = "lock_app_after"
|
||||||
|
|
||||||
@@ -166,6 +174,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val autoUpdateMetadata = "auto_update_metadata"
|
const val autoUpdateMetadata = "auto_update_metadata"
|
||||||
|
|
||||||
|
const val autoUpdateTrackers = "auto_update_trackers"
|
||||||
|
|
||||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
const val showLibraryUpdateErrors = "show_library_update_errors"
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
@@ -189,6 +199,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val unreadBadge = "display_unread_badge"
|
const val unreadBadge = "display_unread_badge"
|
||||||
|
|
||||||
|
const val localBadge = "display_local_badge"
|
||||||
|
|
||||||
const val categoryTabs = "display_category_tabs"
|
const val categoryTabs = "display_category_tabs"
|
||||||
|
|
||||||
const val categoryNumberOfItems = "display_number_of_items"
|
const val categoryNumberOfItems = "display_number_of_items"
|
||||||
@@ -213,8 +225,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val incognitoMode = "incognito_mode"
|
||||||
|
|
||||||
const val createLegacyBackup = "create_legacy_backup"
|
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
@@ -323,12 +333,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
||||||
|
|
||||||
const val mangadexSimilarEnabled = "pref_related_show_tab_key"
|
|
||||||
|
|
||||||
const val mangadexSimilarUpdateInterval = "related_update_interval"
|
|
||||||
|
|
||||||
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
|
|
||||||
|
|
||||||
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
|
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
|
||||||
|
|
||||||
const val preferredMangaDexId = "preferred_mangaDex_id"
|
const val preferredMangaDexId = "preferred_mangaDex_id"
|
||||||
@@ -353,7 +357,7 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
||||||
|
|
||||||
const val biometricTimeRanges = "biometric_time_ranges"
|
const val authenticatorTimeRanges = "biometric_time_ranges"
|
||||||
|
|
||||||
const val sortTagsForLibrary = "sort_tags_for_library"
|
const val sortTagsForLibrary = "sort_tags_for_library"
|
||||||
|
|
||||||
@@ -368,4 +372,16 @@ object PreferenceKeys {
|
|||||||
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
|
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
|
||||||
|
|
||||||
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
|
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
|
||||||
|
|
||||||
|
const val readerBottomButtons = "reader_bottom_buttons"
|
||||||
|
|
||||||
|
const val bottomBarLabels = "pref_show_bottom_bar_labels"
|
||||||
|
|
||||||
|
const val hideUpdatesButton = "pref_hide_updates_button"
|
||||||
|
|
||||||
|
const val hideHistoryButton = "pref_hide_history_button"
|
||||||
|
|
||||||
|
const val pageLayout = "page_layout"
|
||||||
|
|
||||||
|
const val invertDoublePages = "invert_double_pages"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
const val UNMETERED_NETWORK = "wifi"
|
||||||
|
const val CHARGING = "ac"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class stores the values for the preferences in the application.
|
* This class stores the values for the preferences in the application.
|
||||||
*/
|
*/
|
||||||
@@ -18,16 +21,19 @@ object PreferenceValues {
|
|||||||
enum class LightThemeVariant {
|
enum class LightThemeVariant {
|
||||||
default,
|
default,
|
||||||
blue,
|
blue,
|
||||||
|
strawberrydaiquiri,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
// Keys are lowercase to match legacy string values
|
||||||
enum class DarkThemeVariant {
|
enum class DarkThemeVariant {
|
||||||
default,
|
default,
|
||||||
blue,
|
blue,
|
||||||
amoled,
|
greenapple,
|
||||||
red,
|
|
||||||
midnightdusk,
|
midnightdusk,
|
||||||
|
amoled,
|
||||||
hotpink,
|
hotpink,
|
||||||
|
amoledblue,
|
||||||
|
red,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ktlint-enable experimental:enum-entry-name-case */
|
/* ktlint-enable experimental:enum-entry-name-case */
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -36,8 +40,9 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
|||||||
set(get() - item)
|
set(get() - item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Preference<Boolean>.toggle() {
|
fun Preference<Boolean>.toggle(): Boolean {
|
||||||
set(!get())
|
set(!get())
|
||||||
|
return get()
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
@@ -61,9 +66,11 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
||||||
|
|
||||||
fun hideBottomBar() = flowPrefs.getBoolean(Keys.hideBottomBar, true)
|
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
||||||
|
|
||||||
fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
|
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false)
|
||||||
|
|
||||||
|
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
||||||
|
|
||||||
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
||||||
|
|
||||||
@@ -75,9 +82,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
||||||
|
|
||||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||||
|
|
||||||
fun clear() = prefs.edit { clear() }
|
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
||||||
|
|
||||||
@@ -85,8 +92,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
||||||
|
|
||||||
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
|
||||||
|
|
||||||
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
|
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
|
||||||
|
|
||||||
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
|
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
|
||||||
@@ -123,7 +128,11 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
|
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
||||||
|
|
||||||
|
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||||
|
|
||||||
|
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||||
|
|
||||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
||||||
|
|
||||||
@@ -169,6 +178,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
|
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
||||||
|
|
||||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
||||||
@@ -205,6 +216,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
||||||
@@ -217,7 +230,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
||||||
|
|
||||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
||||||
@@ -228,6 +241,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
|
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
|
||||||
|
|
||||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
||||||
|
|
||||||
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
||||||
@@ -295,16 +310,14 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
|
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
|
||||||
|
|
||||||
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE)
|
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.CHAPTER_SORTING_SOURCE)
|
||||||
|
|
||||||
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME)
|
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.CHAPTER_DISPLAY_NAME)
|
||||||
|
|
||||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC)
|
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
||||||
|
|
||||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||||
|
|
||||||
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
|
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||||
@@ -312,7 +325,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
|
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
|
||||||
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
|
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
|
||||||
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
|
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
|
||||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC)
|
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,16 +450,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
||||||
|
|
||||||
fun mangadexSimilarEnabled() = flowPrefs.getBoolean(Keys.mangadexSimilarEnabled, false)
|
|
||||||
|
|
||||||
fun shownMangaDexSimilarAskDialog() = flowPrefs.getBoolean("shown_similar_ask_dialog", false)
|
|
||||||
|
|
||||||
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
|
|
||||||
|
|
||||||
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
|
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
|
||||||
|
|
||||||
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
|
|
||||||
|
|
||||||
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||||
|
|
||||||
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
||||||
@@ -467,7 +472,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
||||||
|
|
||||||
fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
|
fun authenticatorTimeRanges() = flowPrefs.getStringSet(Keys.authenticatorTimeRanges, mutableSetOf())
|
||||||
|
|
||||||
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
||||||
|
|
||||||
@@ -482,4 +487,16 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
|
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
|
||||||
|
|
||||||
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
|
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
|
||||||
|
|
||||||
|
fun readerBottomButtons() = flowPrefs.getStringSet(Keys.readerBottomButtons, ReaderBottomButton.BUTTONS_DEFAULTS)
|
||||||
|
|
||||||
|
fun bottomBarLabels() = flowPrefs.getBoolean(Keys.bottomBarLabels, true)
|
||||||
|
|
||||||
|
fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false)
|
||||||
|
|
||||||
|
fun hideHistoryButton() = flowPrefs.getBoolean(Keys.hideHistoryButton, false)
|
||||||
|
|
||||||
|
fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PagerConfig.PageLayout.SINGLE_PAGE)
|
||||||
|
|
||||||
|
fun invertDoublePages() = flowPrefs.getBoolean(Keys.invertDoublePages, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TrackService that doesn't need explicit login.
|
||||||
|
*/
|
||||||
|
interface NoLoginTrackService {
|
||||||
|
fun loginNoop()
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
@@ -16,9 +17,10 @@ class TrackManager(context: Context) {
|
|||||||
const val KITSU = 3
|
const val KITSU = 3
|
||||||
const val SHIKIMORI = 4
|
const val SHIKIMORI = 4
|
||||||
const val BANGUMI = 5
|
const val BANGUMI = 5
|
||||||
|
const val KOMGA = 6
|
||||||
|
|
||||||
// SY --> Mangadex from Neko
|
// SY --> Mangadex from Neko
|
||||||
const val MDLIST = 6
|
const val MDLIST = 60
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -44,7 +46,9 @@ class TrackManager(context: Context) {
|
|||||||
|
|
||||||
val bangumi = Bangumi(context, BANGUMI)
|
val bangumi = Bangumi(context, BANGUMI)
|
||||||
|
|
||||||
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
|
val komga = Komga(context, KOMGA)
|
||||||
|
|
||||||
|
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi, komga)
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
abstract suspend fun add(track: Track): Track
|
|
||||||
|
|
||||||
abstract suspend fun update(track: Track): Track
|
abstract suspend fun update(track: Track): Track
|
||||||
|
|
||||||
abstract suspend fun bind(track: Track): Track
|
abstract suspend fun bind(track: Track): Track
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Unattended Track Service will never prompt the user to match a manga with the remote.
|
||||||
|
* It is expected that such Track Sercice can only work with specific sources and unique IDs.
|
||||||
|
*/
|
||||||
|
interface UnattendedTrackService {
|
||||||
|
/**
|
||||||
|
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||||
|
*/
|
||||||
|
fun accept(source: Source): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* match is similar to TrackService.search, but only return zero or one match.
|
||||||
|
*/
|
||||||
|
suspend fun match(manga: Manga): TrackSearch?
|
||||||
|
}
|
||||||
@@ -130,7 +130,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
@@ -27,10 +28,14 @@ import kotlinx.serialization.json.putJsonObject
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.addInterceptor(RateLimitInterceptor(85, 1, MINUTES))
|
||||||
|
.build()
|
||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return df.format(track.score)
|
return df.format(track.score)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track, getUserId())
|
return api.addLibManga(track, getUserId())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.komga
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import okhttp3.Dns
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class Komga(private val context: Context, id: Int) : TrackService(id), UnattendedTrackService, NoLoginTrackService {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val UNREAD = 1
|
||||||
|
const val READING = 2
|
||||||
|
const val COMPLETED = 3
|
||||||
|
|
||||||
|
const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val client: OkHttpClient =
|
||||||
|
networkService.client.newBuilder()
|
||||||
|
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val api by lazy { KomgaApi(client) }
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
override fun nameRes() = R.string.tracker_komga
|
||||||
|
|
||||||
|
override fun getLogo() = R.drawable.ic_tracker_komga
|
||||||
|
|
||||||
|
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||||
|
|
||||||
|
override fun getStatusList() = listOf(UNREAD, READING, COMPLETED)
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
UNREAD -> getString(R.string.unread)
|
||||||
|
READING -> getString(R.string.currently_reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> = emptyList()
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String = ""
|
||||||
|
|
||||||
|
override suspend fun update(track: Track): Track {
|
||||||
|
return api.updateProgress(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bind(track: Track): Track {
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
TODO("Not yet implemented: search")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun refresh(track: Track): Track {
|
||||||
|
val remoteTrack = api.getTrackSearch(track.tracking_url)!!
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun login(username: String, password: String) {
|
||||||
|
saveCredentials("user", "pass")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackService.isLogged works by checking that credentials are saved.
|
||||||
|
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
||||||
|
override fun loginNoop() {
|
||||||
|
saveCredentials("user", "pass")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun accept(source: Source): Boolean = source::class.qualifiedName == ACCEPTED_SOURCE
|
||||||
|
|
||||||
|
override suspend fun match(manga: Manga): TrackSearch? =
|
||||||
|
try {
|
||||||
|
api.getTrackSearch(manga.url)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.komga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
const val READLIST_API = "/api/v1/readlists"
|
||||||
|
|
||||||
|
class KomgaApi(private val client: OkHttpClient) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
suspend fun getTrackSearch(url: String): TrackSearch =
|
||||||
|
withIOContext {
|
||||||
|
try {
|
||||||
|
val track = if (url.contains(READLIST_API)) {
|
||||||
|
client.newCall(GET(url))
|
||||||
|
.await()
|
||||||
|
.parseAs<ReadListDto>()
|
||||||
|
.toTrack()
|
||||||
|
} else {
|
||||||
|
client.newCall(GET(url))
|
||||||
|
.await()
|
||||||
|
.parseAs<SeriesDto>()
|
||||||
|
.toTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val progress = client
|
||||||
|
.newCall(GET("$url/read-progress/tachiyomi"))
|
||||||
|
.await()
|
||||||
|
.parseAs<ReadProgressDto>()
|
||||||
|
|
||||||
|
track.apply {
|
||||||
|
cover_url = "$url/thumbnail"
|
||||||
|
tracking_url = url
|
||||||
|
total_chapters = progress.booksCount
|
||||||
|
status = when (progress.booksCount) {
|
||||||
|
progress.booksUnreadCount -> Komga.UNREAD
|
||||||
|
progress.booksReadCount -> Komga.COMPLETED
|
||||||
|
else -> Komga.READING
|
||||||
|
}
|
||||||
|
last_chapter_read = progress.lastReadContinuousIndex
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e, "Could not get item: $url")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateProgress(track: Track): Track {
|
||||||
|
val progress = ReadProgressUpdateDto(track.last_chapter_read)
|
||||||
|
val payload = json.encodeToString(progress)
|
||||||
|
client.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.url("${track.tracking_url}/read-progress/tachiyomi")
|
||||||
|
.put(payload.toRequestBody("application/json".toMediaType()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.await()
|
||||||
|
return getTrackSearch(track.tracking_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
||||||
|
it.title = metadata.title
|
||||||
|
it.summary = metadata.summary
|
||||||
|
it.publishing_status = metadata.status
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
||||||
|
it.title = name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.komga
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeriesDto(
|
||||||
|
val id: String,
|
||||||
|
val libraryId: String,
|
||||||
|
val name: String,
|
||||||
|
val created: String?,
|
||||||
|
val lastModified: String?,
|
||||||
|
val fileLastModified: String,
|
||||||
|
val booksCount: Int,
|
||||||
|
val booksReadCount: Int,
|
||||||
|
val booksUnreadCount: Int,
|
||||||
|
val booksInProgressCount: Int,
|
||||||
|
val metadata: SeriesMetadataDto,
|
||||||
|
val booksMetadata: BookMetadataAggregationDto
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeriesMetadataDto(
|
||||||
|
val status: String,
|
||||||
|
val created: String?,
|
||||||
|
val lastModified: String?,
|
||||||
|
val title: String,
|
||||||
|
val titleSort: String,
|
||||||
|
val summary: String,
|
||||||
|
val summaryLock: Boolean,
|
||||||
|
val readingDirection: String,
|
||||||
|
val readingDirectionLock: Boolean,
|
||||||
|
val publisher: String,
|
||||||
|
val publisherLock: Boolean,
|
||||||
|
val ageRating: Int?,
|
||||||
|
val ageRatingLock: Boolean,
|
||||||
|
val language: String,
|
||||||
|
val languageLock: Boolean,
|
||||||
|
val genres: Set<String>,
|
||||||
|
val genresLock: Boolean,
|
||||||
|
val tags: Set<String>,
|
||||||
|
val tagsLock: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookMetadataAggregationDto(
|
||||||
|
val authors: List<AuthorDto> = emptyList(),
|
||||||
|
val releaseDate: String?,
|
||||||
|
val summary: String,
|
||||||
|
val summaryNumber: String,
|
||||||
|
|
||||||
|
val created: String,
|
||||||
|
val lastModified: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthorDto(
|
||||||
|
val name: String,
|
||||||
|
val role: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReadProgressUpdateDto(
|
||||||
|
val lastBookRead: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReadListDto(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val bookIds: List<String>,
|
||||||
|
val createdDate: String,
|
||||||
|
val lastModifiedDate: String,
|
||||||
|
val filtered: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReadProgressDto(
|
||||||
|
val booksCount: Int,
|
||||||
|
val booksReadCount: Int,
|
||||||
|
val booksUnreadCount: Int,
|
||||||
|
val booksInProgressCount: Int,
|
||||||
|
val lastReadContinuousIndex: Int,
|
||||||
|
)
|
||||||
@@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||||
@@ -17,10 +16,12 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
|||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class MdList(private val context: Context, id: Int) : TrackService(id) {
|
class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
|
private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) }
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
override fun nameRes(): Int = R.string.mdlist
|
override fun nameRes(): Int = R.string.mdlist
|
||||||
@@ -44,8 +45,6 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override fun displayScore(track: Track) = track.score.toInt().toString()
|
override fun displayScore(track: Track) = track.score.toInt().toString()
|
||||||
|
|
||||||
override suspend fun add(track: Track): Track = update(track)
|
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||||
@@ -56,11 +55,14 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
// this updates the follow status in the metadata
|
// this updates the follow status in the metadata
|
||||||
// allow follow status to update
|
// allow follow status to update
|
||||||
if (remoteTrack.status != followStatus.int) {
|
if (remoteTrack.status != followStatus.int) {
|
||||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) {
|
||||||
remoteTrack.status = followStatus.int
|
remoteTrack.status = followStatus.int
|
||||||
|
} else {
|
||||||
|
track.status = remoteTrack.status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.score.toInt() > 0) {
|
/*if (track.score.toInt() > 0) {
|
||||||
mdex.updateRating(track)
|
mdex.updateRating(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
} else if (track.last_chapter_read != 0) {
|
} else if (track.last_chapter_read != 0) {
|
||||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||||
track.last_chapter_read = 0
|
track.last_chapter_read = 0
|
||||||
}
|
}*/
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,9 +98,9 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
/*if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
||||||
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
||||||
}
|
}*/
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,8 +138,5 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
|
override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
|
||||||
|
|
||||||
override val isLogged: Boolean
|
|
||||||
get() = false
|
|
||||||
|
|
||||||
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
track.status = READING
|
track.status = READING
|
||||||
track.score = 0F
|
track.score = 0F
|
||||||
return api.updateItem(track)
|
return api.updateItem(track)
|
||||||
|
|||||||
+3
-8
@@ -11,9 +11,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
|||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private var oauth: OAuth? = null
|
private var oauth: OAuth? = null
|
||||||
set(value) {
|
|
||||||
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
@@ -24,21 +21,19 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
|||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
oauth = myanimelist.loadOAuth()
|
oauth = myanimelist.loadOAuth()
|
||||||
}
|
}
|
||||||
// Refresh access token if null or expired.
|
// Refresh access token if expired
|
||||||
if (oauth!!.isExpired()) {
|
if (oauth != null && oauth!!.isExpired()) {
|
||||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
setAuth(json.decodeFromString(it.body!!.string()))
|
setAuth(json.decodeFromString(it.body!!.string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw on null auth.
|
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw Exception("No authentication token")
|
throw Exception("No authentication token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the authorization header to the original request.
|
// Add the authorization header to the original request
|
||||||
val authRequest = originalRequest.newBuilder()
|
val authRequest = originalRequest.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ data class OAuth(
|
|||||||
val refresh_token: String,
|
val refresh_token: String,
|
||||||
val access_token: String,
|
val access_token: String,
|
||||||
val token_type: String,
|
val token_type: String,
|
||||||
|
val created_at: Long = System.currentTimeMillis(),
|
||||||
val expires_in: Long
|
val expires_in: Long
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun isExpired() = System.currentTimeMillis() > expires_in
|
fun isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun add(track: Track): Track {
|
private suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track, getUsername())
|
return api.addLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import coil.util.CoilUtils
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
@@ -18,30 +21,34 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
/* SY --> */ open /* SY <-- */ val cookieManager = AndroidCookieJar()
|
/* SY --> */ open /* SY <-- */val cookieManager = AndroidCookieJar()
|
||||||
|
|
||||||
/* SY --> */ open /* SY <-- */ val client by lazy {
|
private val baseClientBuilder: OkHttpClient.Builder
|
||||||
val builder = OkHttpClient.Builder()
|
get() {
|
||||||
.cookieJar(cookieManager)
|
val builder = OkHttpClient.Builder()
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
.cookieJar(cookieManager)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.addInterceptor(UserAgentInterceptor())
|
.addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
level = HttpLoggingInterceptor.Level.HEADERS
|
level = HttpLoggingInterceptor.Level.HEADERS
|
||||||
|
}
|
||||||
|
builder.addInterceptor(httpLoggingInterceptor)
|
||||||
}
|
}
|
||||||
builder.addInterceptor(httpLoggingInterceptor)
|
|
||||||
|
when (preferences.dohProvider()) {
|
||||||
|
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||||
|
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
when (preferences.dohProvider()) {
|
/* SY --> */ open /* SY <-- */val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
|
||||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.build()
|
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
|
||||||
}
|
|
||||||
|
|
||||||
/* SY --> */ open /* SY <-- */val cloudflareClient by lazy {
|
/* SY --> */ open /* SY <-- */val cloudflareClient by lazy {
|
||||||
client.newBuilder()
|
client.newBuilder()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@@ -70,7 +71,9 @@ suspend fun Call.await(): Response {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
continuation.resume(response)
|
continuation.resume(response) {
|
||||||
|
response.body?.closeQuietly()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
|||||||
+4
-6
@@ -1,14 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
@@ -114,10 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP error codes are only received since M
|
if (url == origRequestUrl && !challengeFound) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
|
||||||
url == origRequestUrl && !challengeFound
|
|
||||||
) {
|
|
||||||
// The first request didn't return the challenge, abort.
|
// The first request didn't return the challenge, abort.
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
@@ -156,6 +153,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
webView?.stopLoading()
|
webView?.stopLoading()
|
||||||
webView?.destroy()
|
webView?.destroy()
|
||||||
|
webView = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw exception if we failed to bypass Cloudflare
|
// Throw exception if we failed to bypass Cloudflare
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OkHttp interceptor that handles rate limiting.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||||
|
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
||||||
|
*
|
||||||
|
* @param permits {Int} Number of requests allowed within a period of units.
|
||||||
|
* @param period {Long} The limiting duration. Defaults to 1.
|
||||||
|
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||||
|
*/
|
||||||
|
class RateLimitInterceptor(
|
||||||
|
private val permits: Int,
|
||||||
|
private val period: Long = 1,
|
||||||
|
private val unit: TimeUnit = TimeUnit.SECONDS
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private val requestQueue = ArrayList<Long>(permits)
|
||||||
|
private val rateLimitMillis = unit.toMillis(period)
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
synchronized(requestQueue) {
|
||||||
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
val waitTime = if (requestQueue.size < permits) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
val oldestReq = requestQueue[0]
|
||||||
|
val newestReq = requestQueue[permits - 1]
|
||||||
|
|
||||||
|
if (newestReq - oldestReq > rateLimitMillis) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
oldestReq + rateLimitMillis - now // Remaining time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestQueue.size == permits) {
|
||||||
|
requestQueue.removeAt(0)
|
||||||
|
}
|
||||||
|
if (waitTime > 0) {
|
||||||
|
requestQueue.add(now + waitTime)
|
||||||
|
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||||
|
} else {
|
||||||
|
requestQueue.add(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||||
|
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||||
|
*
|
||||||
|
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||||
|
* @param permits {Int} Number of requests allowed within a period of units.
|
||||||
|
* @param period {Long} The limiting duration. Defaults to 1.
|
||||||
|
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||||
|
*/
|
||||||
|
class SpecificHostRateLimitInterceptor(
|
||||||
|
private val httpUrl: HttpUrl,
|
||||||
|
private val permits: Int,
|
||||||
|
private val period: Long = 1,
|
||||||
|
private val unit: TimeUnit = TimeUnit.SECONDS
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private val requestQueue = ArrayList<Long>(permits)
|
||||||
|
private val rateLimitMillis = unit.toMillis(period)
|
||||||
|
private val host = httpUrl.host
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
if (chain.request().url.host != host) {
|
||||||
|
return chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
synchronized(requestQueue) {
|
||||||
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
val waitTime = if (requestQueue.size < permits) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
val oldestReq = requestQueue[0]
|
||||||
|
val newestReq = requestQueue[permits - 1]
|
||||||
|
|
||||||
|
if (newestReq - oldestReq > rateLimitMillis) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
oldestReq + rateLimitMillis - now // Remaining time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestQueue.size == permits) {
|
||||||
|
requestQueue.removeAt(0)
|
||||||
|
}
|
||||||
|
if (waitTime > 0) {
|
||||||
|
requestQueue.add(now + waitTime)
|
||||||
|
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||||
|
} else {
|
||||||
|
requestQueue.add(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
@@ -38,8 +38,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
private const val COVER_NAME = "cover.jpg"
|
private const val COVER_NAME = "cover.jpg"
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(OrderBy())
|
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||||
@@ -248,12 +246,10 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
ChapterRecognition.parseChapterNumber(this, manga)
|
ChapterRecognition.parseChapterNumber(this, manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sortedWith(
|
.sortedWith { c1, c2 ->
|
||||||
Comparator { c1, c2 ->
|
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
return Observable.just(chapters)
|
return Observable.just(chapters)
|
||||||
@@ -298,7 +294,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
return extension.toLowerCase(Locale.ROOT) in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
@@ -310,7 +306,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
return getFormat(chapFile)
|
return getFormat(chapFile)
|
||||||
}
|
}
|
||||||
throw Exception("Chapter not found")
|
throw Exception(context.getString(R.string.chapter_not_found))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFormat(file: File): Format {
|
private fun getFormat(file: File): Format {
|
||||||
@@ -324,7 +320,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
} else if (extension.equals("epub", true)) {
|
} else if (extension.equals("epub", true)) {
|
||||||
Format.Epub(file)
|
Format.Epub(file)
|
||||||
} else {
|
} else {
|
||||||
throw Exception("Invalid chapter format")
|
throw Exception(context.getString(R.string.local_invalid_format))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,9 +363,16 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
|
override fun getFilterList() = POPULAR_FILTERS
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(OrderBy())
|
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||||
|
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||||
|
|
||||||
|
private class OrderBy(context: Context) : Filter.Sort(
|
||||||
|
context.getString(R.string.local_filter_order_by),
|
||||||
|
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||||
|
Selection(0, true)
|
||||||
|
)
|
||||||
|
|
||||||
sealed class Format {
|
sealed class Format {
|
||||||
data class Directory(val file: File) : Format()
|
data class Directory(val file: File) : Format()
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ sealed class Filter<T>(val name: String, var state: T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
abstract class AutoComplete(name: String, val hint: String, val values: List<String>, val skipAutoFillTags: List<String> = emptyList(), val excludePrefix: String? = null, state: List<String>) : Filter<List<String>>(name, state)
|
abstract class AutoComplete(
|
||||||
|
name: String,
|
||||||
|
val hint: String,
|
||||||
|
val values: List<String>,
|
||||||
|
val skipAutoFillTags: List<String> = emptyList(),
|
||||||
|
val excludePrefix: String? = null,
|
||||||
|
state: List<String>
|
||||||
|
) : Filter<List<String>>(name, state)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
fun copy(mangas: List<SManga> = this.mangas, hasNextPage: Boolean = this.hasNextPage): MangasPage {
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ open class Page(
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
var imageUrl = imageUrl
|
var imageUrl = imageUrl
|
||||||
get() {
|
get() = field?.let { DataSaver.compress(it) }
|
||||||
return field?.let { DataSaver.compress(it) }
|
|
||||||
}
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val number: Int
|
val number: Int
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
|
|
||||||
interface BrowseSourceFilterHeader : CatalogueSource {
|
interface BrowseSourceFilterHeader : CatalogueSource {
|
||||||
fun getFilterHeader(controller: BaseController<*>): RecyclerView.Adapter<*>
|
fun getFilterHeader(controller: BaseController<*>, onClick: () -> Unit): RecyclerView.Adapter<*>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import exh.md.utils.FollowStatus
|
|||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
|
|
||||||
interface FollowsSource : CatalogueSource {
|
interface FollowsSource : CatalogueSource {
|
||||||
suspend fun fetchFollows(): MangasPage
|
suspend fun fetchFollows(page: Int): MangasPage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of all Follows retrieved by Coroutines
|
* Returns a list of all Follows retrieved by Coroutines
|
||||||
*
|
*
|
||||||
* @param SManga all smanga found for user
|
* @param SManga all smanga found for user
|
||||||
*/
|
*/
|
||||||
suspend fun fetchAllFollows(forceHd: Boolean = false): List<Pair<SManga, RaisedSearchMetadata>>
|
suspend fun fetchAllFollows(): List<Pair<SManga, RaisedSearchMetadata>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* updates the follow status for a manga
|
* updates the follow status for a manga
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||||
|
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataCompletable
|
|
||||||
import exh.util.executeOnIO
|
import exh.util.executeOnIO
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
@@ -34,9 +36,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Parse the supplied input into the supplied metadata object
|
* Parse the supplied input into the supplied metadata object
|
||||||
*/
|
*/
|
||||||
fun parseIntoMetadata(metadata: M, input: I)
|
suspend fun parseIntoMetadata(metadata: M, input: I)
|
||||||
|
|
||||||
suspend fun parseInfoIntoMetadata(metadata: M, input: I) = parseIntoMetadata(metadata, input)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use reflection to create a new instance of metadata
|
* Use reflection to create a new instance of metadata
|
||||||
@@ -51,31 +51,11 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* Will also save the metadata to the DB if possible
|
* Will also save the metadata to the DB if possible
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||||
@Deprecated("Use the MangaInfo variant")
|
@Deprecated("Use the MangaInfo variant")
|
||||||
fun parseToManga(manga: SManga, input: I): Completable {
|
fun parseToManga(manga: SManga, input: I): Completable = runAsObservable({
|
||||||
val mangaId = manga.id
|
parseToManga(manga.toMangaInfo(), input)
|
||||||
val metaObservable = if (mangaId != null) {
|
}).toCompletable()
|
||||||
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
|
|
||||||
Single.fromCallable {
|
|
||||||
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
|
||||||
}.map {
|
|
||||||
it?.raise(metaClass) ?: newMetaInstance()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Single.just(newMetaInstance())
|
|
||||||
}
|
|
||||||
|
|
||||||
return metaObservable.map {
|
|
||||||
parseIntoMetadata(it, input)
|
|
||||||
it.copyTo(manga)
|
|
||||||
it
|
|
||||||
}.flatMapCompletable {
|
|
||||||
if (mangaId != null) {
|
|
||||||
it.mangaId = mangaId
|
|
||||||
db.insertFlatMetadataCompletable(it.flatten())
|
|
||||||
} else Completable.complete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun parseToManga(manga: MangaInfo, input: I): MangaInfo {
|
suspend fun parseToManga(manga: MangaInfo, input: I): MangaInfo {
|
||||||
val mangaId = manga.id()
|
val mangaId = manga.id()
|
||||||
@@ -84,7 +64,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
flatMetadata?.raise(metaClass) ?: newMetaInstance()
|
flatMetadata?.raise(metaClass) ?: newMetaInstance()
|
||||||
} else newMetaInstance()
|
} else newMetaInstance()
|
||||||
|
|
||||||
parseInfoIntoMetadata(metadata, input)
|
parseIntoMetadata(metadata, input)
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
metadata.mangaId = mangaId
|
metadata.mangaId = mangaId
|
||||||
db.insertFlatMetadata(metadata.flatten())
|
db.insertFlatMetadata(metadata.flatten())
|
||||||
@@ -100,31 +80,12 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
||||||
* also be saved to the DB.
|
* also be saved to the DB.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||||
@Deprecated("use fetchOrLoadMetadata made for MangaInfo")
|
@Deprecated("use fetchOrLoadMetadata made for MangaInfo")
|
||||||
fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> {
|
fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> =
|
||||||
val metaObservable = if (mangaId != null) {
|
runAsObservable({
|
||||||
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
|
fetchOrLoadMetadata(mangaId) { inputProducer().toObservable().awaitSingle() }
|
||||||
Single.fromCallable {
|
}).toSingle()
|
||||||
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
|
||||||
}.map {
|
|
||||||
it?.raise(metaClass)
|
|
||||||
}
|
|
||||||
} else Single.just(null)
|
|
||||||
|
|
||||||
return metaObservable.flatMap { existingMeta ->
|
|
||||||
if (existingMeta == null) {
|
|
||||||
inputProducer().flatMap { input ->
|
|
||||||
val newMeta = newMetaInstance()
|
|
||||||
parseIntoMetadata(newMeta, input)
|
|
||||||
val newMetaSingle = Single.just(newMeta)
|
|
||||||
if (mangaId != null) {
|
|
||||||
newMeta.mangaId = mangaId
|
|
||||||
db.insertFlatMetadataCompletable(newMeta.flatten()).andThen(newMetaSingle)
|
|
||||||
} else newMetaSingle
|
|
||||||
}
|
|
||||||
} else Single.just(existingMeta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
||||||
@@ -143,7 +104,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
|
|
||||||
return meta ?: inputProducer().let { input ->
|
return meta ?: inputProducer().let { input ->
|
||||||
val newMeta = newMetaInstance()
|
val newMeta = newMetaInstance()
|
||||||
parseInfoIntoMetadata(newMeta, input)
|
parseIntoMetadata(newMeta, input)
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
newMeta.mangaId = mangaId
|
newMeta.mangaId = mangaId
|
||||||
db.insertFlatMetadata(newMeta.flatten()).let { newMeta }
|
db.insertFlatMetadata(newMeta.flatten()).let { newMeta }
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toChapterInfo
|
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -24,7 +25,7 @@ import eu.kanade.tachiyomi.source.online.NamespaceSource
|
|||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.eh.EHTags
|
import exh.eh.EHTags
|
||||||
import exh.eh.EHentaiUpdateHelper
|
import exh.eh.EHentaiUpdateHelper
|
||||||
@@ -45,13 +46,13 @@ import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
|||||||
import exh.util.UriFilter
|
import exh.util.UriFilter
|
||||||
import exh.util.UriGroup
|
import exh.util.UriGroup
|
||||||
import exh.util.asObservableWithAsyncStacktrace
|
import exh.util.asObservableWithAsyncStacktrace
|
||||||
|
import exh.util.awaitResponse
|
||||||
import exh.util.dropBlank
|
import exh.util.dropBlank
|
||||||
import exh.util.ignore
|
import exh.util.ignore
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
import exh.util.trimAll
|
import exh.util.trimAll
|
||||||
import exh.util.trimOrNull
|
import exh.util.trimOrNull
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@@ -75,7 +76,6 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -179,38 +179,43 @@ class EHentai(
|
|||||||
tags += parsedTags
|
tags += parsedTags
|
||||||
|
|
||||||
if (infoElements != null) {
|
if (infoElements != null) {
|
||||||
getGenre(infoElements.getOrNull(1))?.let { genre = it }
|
genre = getGenre(infoElements.getOrNull(1))
|
||||||
|
|
||||||
getDateTag(infoElements.getOrNull(2))?.let { datePosted = it }
|
datePosted = getDateTag(infoElements.getOrNull(2))
|
||||||
|
|
||||||
getRating(infoElements.getOrNull(3))?.let { averageRating = it }
|
averageRating = getRating(infoElements.getOrNull(3))
|
||||||
|
|
||||||
getUploader(infoElements.getOrNull(4))?.let { uploader = it }
|
uploader = getUploader(infoElements.getOrNull(4))
|
||||||
|
|
||||||
getPageCount(infoElements.getOrNull(5))?.let { length = it }
|
length = getPageCount(infoElements.getOrNull(5))
|
||||||
} else {
|
} else {
|
||||||
val parsedGenre = body.selectFirst(".gl1c div")
|
val parsedGenre = body.selectFirst(".gl1c div")
|
||||||
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
|
genre = getGenre(
|
||||||
|
genreString = parsedGenre?.text()
|
||||||
|
?.nullIfBlank()
|
||||||
|
?.toLowerCase()
|
||||||
|
?.replace(" ", "")
|
||||||
|
)
|
||||||
|
|
||||||
val info = body.selectFirst(".gl2c")
|
val info = body.selectFirst(".gl2c")
|
||||||
val extraInfo = body.selectFirst(".gl4c")
|
val extraInfo = body.selectFirst(".gl4c")
|
||||||
|
|
||||||
val infoList = info.select("div div")
|
val infoList = info.select("div div")
|
||||||
|
|
||||||
getDateTag(infoList.getOrNull(8))?.let { datePosted = it }
|
datePosted = getDateTag(infoList.getOrNull(8))
|
||||||
|
|
||||||
getRating(infoList.getOrNull(9))?.let { averageRating = it }
|
averageRating = getRating(infoList.getOrNull(9))
|
||||||
|
|
||||||
val extraInfoList = extraInfo.select("div")
|
val extraInfoList = extraInfo.select("div")
|
||||||
|
|
||||||
if (extraInfoList.getOrNull(2) == null) {
|
if (extraInfoList.getOrNull(2) == null) {
|
||||||
getUploader(extraInfoList.getOrNull(0))?.let { uploader = it }
|
uploader = getUploader(extraInfoList.getOrNull(0))
|
||||||
|
|
||||||
getPageCount(extraInfoList.getOrNull(1))?.let { length = it }
|
length = getPageCount(extraInfoList.getOrNull(1))
|
||||||
} else {
|
} else {
|
||||||
getUploader(extraInfoList.getOrNull(1))?.let { uploader = it }
|
uploader = getUploader(extraInfoList.getOrNull(1))
|
||||||
|
|
||||||
getPageCount(extraInfoList.getOrNull(2))?.let { length = it }
|
length = getPageCount(extraInfoList.getOrNull(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,93 +289,94 @@ class EHentai(
|
|||||||
|
|
||||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> = getChapterList(manga) {}
|
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> = getChapterList(manga) {}
|
||||||
|
|
||||||
suspend fun getChapterList(manga: MangaInfo, throttleFunc: () -> Unit) = fetchChapterList(manga.toSManga(), throttleFunc).awaitSingle().map { it.toChapterInfo() }
|
suspend fun getChapterList(manga: MangaInfo, throttleFunc: suspend () -> Unit): List<ChapterInfo> {
|
||||||
|
// Pull all the way to the root gallery
|
||||||
|
// We can't do this with RxJava or we run into stack overflows on shit like this:
|
||||||
|
// https://exhentai.org/g/1073061/f9345f1c12/
|
||||||
|
var url = manga.key
|
||||||
|
var doc: Document
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
|
while (true) {
|
||||||
|
val gid = EHentaiSearchMetadata.galleryId(url).toInt()
|
||||||
|
val cachedParent = updateHelper.parentLookupTable.get(
|
||||||
|
gid
|
||||||
|
)
|
||||||
|
if (cachedParent == null) {
|
||||||
|
throttleFunc()
|
||||||
|
doc = client.newCall(exGet(baseUrl + url)).await().asJsoup()
|
||||||
|
|
||||||
@Deprecated("Use getChapterList instead")
|
val parentLink = doc.select("#gdd .gdt1").find { el ->
|
||||||
fun fetchChapterList(manga: SManga, throttleFunc: () -> Unit): Observable<List<SChapter>> {
|
el.text().toLowerCase() == "parent:"
|
||||||
return Single.fromCallable {
|
}!!.nextElementSibling().selectFirst("a")?.attr("href")
|
||||||
// Pull all the way to the root gallery
|
|
||||||
// We can't do this with RxJava or we run into stack overflows on shit like this:
|
|
||||||
// https://exhentai.org/g/1073061/f9345f1c12/
|
|
||||||
var url: String = manga.url
|
|
||||||
var doc: Document? = null
|
|
||||||
|
|
||||||
runBlocking {
|
if (parentLink != null) {
|
||||||
while (true) {
|
updateHelper.parentLookupTable.put(
|
||||||
val gid = EHentaiSearchMetadata.galleryId(url).toInt()
|
gid,
|
||||||
val cachedParent = updateHelper.parentLookupTable.get(
|
GalleryEntry(
|
||||||
gid
|
EHentaiSearchMetadata.galleryId(parentLink),
|
||||||
)
|
EHentaiSearchMetadata.galleryToken(parentLink)
|
||||||
if (cachedParent == null) {
|
|
||||||
throttleFunc()
|
|
||||||
|
|
||||||
val resp = client.newCall(exGet(baseUrl + url)).execute()
|
|
||||||
if (!resp.isSuccessful) error("HTTP error (${resp.code})!")
|
|
||||||
doc = resp.asJsoup()
|
|
||||||
|
|
||||||
val parentLink = doc!!.select("#gdd .gdt1").find { el ->
|
|
||||||
el.text().toLowerCase() == "parent:"
|
|
||||||
}!!.nextElementSibling().selectFirst("a")?.attr("href")
|
|
||||||
|
|
||||||
if (parentLink != null) {
|
|
||||||
updateHelper.parentLookupTable.put(
|
|
||||||
gid,
|
|
||||||
GalleryEntry(
|
|
||||||
EHentaiSearchMetadata.galleryId(parentLink),
|
|
||||||
EHentaiSearchMetadata.galleryToken(parentLink)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
|
|
||||||
} else break
|
|
||||||
} else {
|
|
||||||
this@EHentai.xLogD("Parent cache hit: %s!", gid)
|
|
||||||
url = EHentaiSearchMetadata.idAndTokenToUrl(
|
|
||||||
cachedParent.gId,
|
|
||||||
cachedParent.gToken
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
url = EHentaiSearchMetadata.normalizeUrl(parentLink)
|
||||||
|
} else break
|
||||||
|
} else {
|
||||||
|
this@EHentai.xLogD("Parent cache hit: %s!", gid)
|
||||||
|
url = EHentaiSearchMetadata.idAndTokenToUrl(
|
||||||
|
cachedParent.gId,
|
||||||
|
cachedParent.gToken
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
doc!!
|
val newDisplay = doc.select("#gnd a")
|
||||||
}.map { d ->
|
// Build chapter for root gallery
|
||||||
val newDisplay = d.select("#gnd a")
|
val self = ChapterInfo(
|
||||||
// Build chapter for root gallery
|
key = EHentaiSearchMetadata.normalizeUrl(doc.location()),
|
||||||
val self = SChapter.create().apply {
|
name = "v1: " + doc.selectFirst("#gn").text(),
|
||||||
url = EHentaiSearchMetadata.normalizeUrl(d.location())
|
number = 1f,
|
||||||
name = "v1: " + d.selectFirst("#gn").text()
|
dateUpload = MetadataUtil.EX_DATE_FORMAT.parse(
|
||||||
chapter_number = 1f
|
doc.select("#gdd .gdt1").find { el ->
|
||||||
date_upload = MetadataUtil.EX_DATE_FORMAT.parse(
|
el.text().toLowerCase() == "posted:"
|
||||||
d.select("#gdd .gdt1").find { el ->
|
}!!.nextElementSibling().text()
|
||||||
el.text().toLowerCase() == "posted:"
|
)!!.time
|
||||||
}!!.nextElementSibling().text()
|
)
|
||||||
)!!.time
|
// Build and append the rest of the galleries
|
||||||
}
|
return if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) {
|
||||||
// Build and append the rest of the galleries
|
listOf(self)
|
||||||
if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) listOf(self)
|
} else {
|
||||||
else {
|
newDisplay.mapIndexed { index, newGallery ->
|
||||||
newDisplay.mapIndexed { index, newGallery ->
|
val link = newGallery.attr("href")
|
||||||
val link = newGallery.attr("href")
|
val name = newGallery.text()
|
||||||
val name = newGallery.text()
|
val posted = (newGallery.nextSibling() as TextNode).text().removePrefix(", added ")
|
||||||
val posted = (newGallery.nextSibling() as TextNode).text().removePrefix(", added ")
|
ChapterInfo(
|
||||||
SChapter.create().apply {
|
key = EHentaiSearchMetadata.normalizeUrl(link),
|
||||||
this.url = EHentaiSearchMetadata.normalizeUrl(link)
|
name = "v${index + 2}: $name",
|
||||||
this.name = "v${index + 2}: $name"
|
number = index + 2f,
|
||||||
this.chapter_number = index + 2f
|
dateUpload = MetadataUtil.EX_DATE_FORMAT.parse(posted)!!.time
|
||||||
this.date_upload = MetadataUtil.EX_DATE_FORMAT.parse(posted)!!.time
|
)
|
||||||
}
|
}.reversed() + self
|
||||||
}.reversed() + self
|
}
|
||||||
}
|
|
||||||
}.toObservable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, baseUrl + chapter.url).map {
|
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||||
it.mapIndexed { i, s ->
|
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
|
||||||
Page(i, s)
|
|
||||||
|
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||||
|
@Deprecated("Use getChapterList instead")
|
||||||
|
fun fetchChapterList(manga: SManga, throttleFunc: suspend () -> Unit) = runAsObservable({
|
||||||
|
getChapterList(manga.toMangaInfo(), throttleFunc).map { it.toSChapter() }
|
||||||
|
})
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, baseUrl + chapter.url)
|
||||||
|
.map {
|
||||||
|
it.mapIndexed { i, s ->
|
||||||
|
Page(i, s)
|
||||||
|
}
|
||||||
|
}!!
|
||||||
|
.doOnNext { pages ->
|
||||||
|
if (pages.any { it.url == "https://$domain/img/509.gif" }) throw Exception(
|
||||||
|
"Hit page limit"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}!!.doOnNext { pages -> if (pages.any { it.url == "https://$domain/img/509.gif" }) throw Exception("Hit page limit") }
|
|
||||||
|
|
||||||
private fun fetchChapterPage(
|
private fun fetchChapterPage(
|
||||||
chapter: SChapter,
|
chapter: SChapter,
|
||||||
@@ -465,23 +471,23 @@ class EHentai(
|
|||||||
|
|
||||||
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
|
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
|
||||||
return GET(
|
return GET(
|
||||||
page?.let {
|
if (page != null) {
|
||||||
addParam(url, "page", (page - 1).toString())
|
addParam(url, "page", (page - 1).toString())
|
||||||
} ?: url,
|
} else url,
|
||||||
additionalHeaders?.let { additionalHeadersNotNull ->
|
if (additionalHeaders != null) {
|
||||||
val headers = headers.newBuilder()
|
val headers = headers.newBuilder()
|
||||||
additionalHeadersNotNull.toMultimap().forEach { (t, u) ->
|
additionalHeaders.toMultimap().forEach { (t, u) ->
|
||||||
u.forEach {
|
u.forEach {
|
||||||
headers.add(t, it)
|
headers.add(t, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headers.build()
|
headers.build()
|
||||||
} ?: headers
|
} else headers
|
||||||
).let {
|
).let {
|
||||||
if (!cache) {
|
if (cache) {
|
||||||
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
|
|
||||||
} else {
|
|
||||||
it
|
it
|
||||||
|
} else {
|
||||||
|
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,7 +535,7 @@ class EHentai(
|
|||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val exception = Exception("Async stacktrace")
|
val exception = Exception("Async stacktrace")
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).awaitResponse()
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
// Pull to most recent
|
// Pull to most recent
|
||||||
val doc = response.asJsoup()
|
val doc = response.asJsoup()
|
||||||
@@ -557,7 +563,7 @@ class EHentai(
|
|||||||
*/
|
*/
|
||||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: EHentaiSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
with(input) {
|
with(input) {
|
||||||
val url = location()
|
val url = location()
|
||||||
@@ -856,13 +862,12 @@ 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.trimAll().dropBlank()
|
filter.state.trimAll().dropBlank().mapNotNull { tag ->
|
||||||
splitState.mapNotNull { tag ->
|
|
||||||
val split = tag.split(":").filterNot { it.isBlank() }
|
val split = tag.split(":").filterNot { it.isBlank() }
|
||||||
if (split.size > 1) {
|
if (split.size > 1) {
|
||||||
val namespace = split[0].removePrefix("-")
|
val namespace = split[0].removePrefix("-")
|
||||||
val exclude = split[0].startsWith("-")
|
val exclude = split[0].startsWith("-")
|
||||||
AdvSearchEntry(Pair(namespace, split[1]), exclude)
|
AdvSearchEntry(namespace to split[1], exclude)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -872,7 +877,7 @@ class EHentai(
|
|||||||
advSearch.forEach { entry ->
|
advSearch.forEach { entry ->
|
||||||
if (entry.exclude) stringBuilder.append("-")
|
if (entry.exclude) stringBuilder.append("-")
|
||||||
if (entry.search.second.contains(" ")) {
|
if (entry.search.second.contains(" ")) {
|
||||||
stringBuilder.append(("${entry.search.first}:\"${entry.search.second}$\""))
|
stringBuilder.append(("""${entry.search.first}:"${entry.search.second}$""""))
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append("${entry.search.first}:${entry.search.second}$")
|
stringBuilder.append("${entry.search.first}:${entry.search.second}$")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -22,7 +19,6 @@ import exh.source.DelegatedHttpSource
|
|||||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -36,25 +32,17 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
|||||||
override val lang = if (id == otherId) "all" else delegate.lang
|
override val lang = if (id == otherId) "all" else delegate.lang
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response.asJsoup())
|
return parseToManga(manga, response.asJsoup())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
url = input.location()
|
url = input.location()
|
||||||
|
|
||||||
@@ -68,9 +56,9 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
|||||||
artists = galleryElement.select("h2 a").map { it.text() }
|
artists = galleryElement.select("h2 a").map { it.text() }
|
||||||
tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
|
tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
|
||||||
|
|
||||||
input.select(".gallery-info tr").forEach {
|
input.select(".gallery-info tr").forEach { galleryInfoElement ->
|
||||||
val content = it.child(1)
|
val content = galleryInfoElement.child(1)
|
||||||
when (it.child(0).text().toLowerCase()) {
|
when (galleryInfoElement.child(0).text().toLowerCase()) {
|
||||||
"group" -> {
|
"group" -> {
|
||||||
group = content.text()
|
group = content.text()
|
||||||
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||||
@@ -103,9 +91,11 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
"tags" -> {
|
"tags" -> {
|
||||||
tags += content.select("a").map {
|
tags += content.select("a").map {
|
||||||
val ns = if (it.attr("href").startsWith("/tag/male")) "male"
|
val ns = when {
|
||||||
else if (it.attr("href").startsWith("/tag/female")) "female"
|
it.attr("href").startsWith("/tag/male") -> "male"
|
||||||
else "misc"
|
it.attr("href").startsWith("/tag/female") -> "female"
|
||||||
|
else -> "misc"
|
||||||
|
}
|
||||||
RaisedTag(
|
RaisedTag(
|
||||||
ns,
|
ns,
|
||||||
it.text().dropLast(if (ns == "misc") 0 else 2),
|
it.text().dropLast(if (ns == "misc") 0 else 2),
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
@@ -21,14 +16,10 @@ import eu.kanade.tachiyomi.source.online.FollowsSource
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
|
||||||
import exh.GalleryAddEvent
|
|
||||||
import exh.GalleryAdder
|
|
||||||
import exh.md.MangaDexFabHeaderAdapter
|
import exh.md.MangaDexFabHeaderAdapter
|
||||||
import exh.md.handlers.ApiChapterParser
|
import exh.md.handlers.ApiChapterParser
|
||||||
import exh.md.handlers.ApiMangaParser
|
import exh.md.handlers.ApiMangaParser
|
||||||
@@ -36,76 +27,93 @@ import exh.md.handlers.FollowsHandler
|
|||||||
import exh.md.handlers.MangaHandler
|
import exh.md.handlers.MangaHandler
|
||||||
import exh.md.handlers.MangaPlusHandler
|
import exh.md.handlers.MangaPlusHandler
|
||||||
import exh.md.handlers.SimilarHandler
|
import exh.md.handlers.SimilarHandler
|
||||||
|
import exh.md.network.MangaDexLoginHelper
|
||||||
|
import exh.md.network.NoSessionException
|
||||||
|
import exh.md.network.TokenAuthenticator
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdLang
|
import exh.md.utils.MdLang
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import okio.EOFException
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OverridingDeprecatedMember")
|
||||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
MetadataSource<MangaDexSearchMetadata, Response>,
|
MetadataSource<MangaDexSearchMetadata, Response>,
|
||||||
UrlImportableSource,
|
// UrlImportableSource,
|
||||||
FollowsSource,
|
FollowsSource,
|
||||||
LoginSource,
|
LoginSource,
|
||||||
BrowseSourceFilterHeader,
|
BrowseSourceFilterHeader,
|
||||||
RandomMangaSource {
|
RandomMangaSource,
|
||||||
|
NamespaceSource {
|
||||||
override val lang: String = delegate.lang
|
override val lang: String = delegate.lang
|
||||||
|
|
||||||
override val headers: Headers = super.headers.newBuilder().apply {
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
add("Referer", MdUtil.baseUrl)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
private val mdLang by lazy {
|
private val mdLang by lazy {
|
||||||
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
MdLang.fromExt(lang) ?: MdLang.ENGLISH
|
||||||
}
|
}
|
||||||
|
|
||||||
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 preferences = Injekt.get<PreferencesHelper>()
|
||||||
val trackManager: TrackManager by injectLazy()
|
val mdList: MdList = Injekt.get<TrackManager>().mdList
|
||||||
|
|
||||||
private val sourcePreferences: SharedPreferences by lazy {
|
/*private val sourcePreferences: SharedPreferences by lazy {
|
||||||
context.getSharedPreferences("source_$id", 0x0000)
|
context.getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
private val loginHelper = MangaDexLoginHelper(networkHttpClient, preferences, mdList)
|
||||||
|
|
||||||
|
override val baseHttpClient: OkHttpClient = super.client.newBuilder()
|
||||||
|
.authenticator(
|
||||||
|
TokenAuthenticator(loginHelper)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun useLowQualityThumbnail() = false // sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
|
||||||
|
|
||||||
|
private val apiMangaParser by lazy {
|
||||||
|
ApiMangaParser(baseHttpClient, mdLang.lang)
|
||||||
|
}
|
||||||
|
private val apiChapterParser by lazy {
|
||||||
|
ApiChapterParser()
|
||||||
|
}
|
||||||
|
private val followsHandler by lazy {
|
||||||
|
FollowsHandler(baseHttpClient, headers, preferences, mdLang.lang, mdList)
|
||||||
|
}
|
||||||
|
private val mangaHandler by lazy {
|
||||||
|
MangaHandler(baseHttpClient, headers, mdLang.lang, apiMangaParser, followsHandler)
|
||||||
|
}
|
||||||
|
private val similarHandler by lazy {
|
||||||
|
SimilarHandler(baseHttpClient, mdLang.lang)
|
||||||
|
}
|
||||||
|
private val mangaPlusHandler by lazy {
|
||||||
|
MangaPlusHandler(network.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun useLowQualityThumbnail() = sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
|
/*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) {
|
||||||
importIdToMdId(query) {
|
importIdToMdId(query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
/*override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||||
|
|
||||||
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
|
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
|
||||||
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
|
"/manga/" + uri.pathSegments[1]
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -119,45 +127,44 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
|
|
||||||
override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? {
|
override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? {
|
||||||
val id = uri.pathSegments.getOrNull(2) ?: return null
|
val id = uri.pathSegments.getOrNull(2) ?: return null
|
||||||
val mangaId = MangaHandler(client, headers, mdLang).getMangaIdFromChapterId(id)
|
val mangaId = MangaHandler(baseHttpClient, headers, mdLang).getMangaIdFromChapterId(id)
|
||||||
return MdUtil.mapMdIdToMangaUrl(mangaId)
|
return MdUtil.mapMdIdToMangaUrl(mangaId)
|
||||||
}
|
}*/
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
|
return mangaHandler.fetchMangaDetailsObservable(manga, id, preferences.mangaDexForceLatestCovers().get())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id)
|
return mangaHandler.getMangaDetails(manga, id, preferences.mangaDexForceLatestCovers().get())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
|
return mangaHandler.fetchChapterListObservable(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getChapterList(manga)
|
return mangaHandler.getChapterList(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return if (chapter.scanlator == "MangaPlus") {
|
return if (chapter.scanlator == "MangaPlus") {
|
||||||
client.newCall(mangaPlusPageListRequest(chapter))
|
mangaPlusHandler.client.newCall(mangaPlusPageListRequest(chapter))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
val chapterId = ApiChapterParser().externalParse(response)
|
val chapterId = apiChapterParser.externalParse(response)
|
||||||
MangaPlusHandler(client).fetchPageList(chapterId)
|
mangaPlusHandler.fetchPageList(chapterId)
|
||||||
}
|
}
|
||||||
} else super.fetchPageList(chapter)
|
} else super.fetchPageList(chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
|
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
|
||||||
val urlChapterId = MdUtil.getChapterId(chapter.url)
|
return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK)
|
||||||
return GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchImage(page: Page): Observable<Response> {
|
override fun fetchImage(page: Page): Observable<Response> {
|
||||||
return if (page.imageUrl!!.contains("mangaplus", true)) {
|
return if (page.imageUrl?.contains("mangaplus", true) == true) {
|
||||||
MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
|
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
} else super.fetchImage(page)
|
} else super.fetchImage(page)
|
||||||
}
|
}
|
||||||
@@ -168,29 +175,28 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
return MangaDexDescriptionAdapter(controller)
|
return MangaDexDescriptionAdapter(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
||||||
ApiMangaParser(mdLang).parseIntoMetadata(metadata, input, emptyList())
|
apiMangaParser.parseIntoMetadata(metadata, input, emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchFollows(): MangasPage {
|
override suspend fun fetchFollows(page: Int): MangasPage {
|
||||||
return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
|
return followsHandler.fetchFollows(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val requiresLogin: Boolean = true
|
override val requiresLogin: Boolean = false
|
||||||
|
|
||||||
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED
|
override val twoFactorAuth = LoginSource.AuthSupport.NOT_SUPPORTED
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
override fun isLogged(): Boolean {
|
||||||
val httpUrl = MdUtil.baseUrl.toHttpUrl()
|
return mdList.isLogged
|
||||||
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUsername(): String {
|
override fun getUsername(): String {
|
||||||
return trackManager.mdList.getUsername()
|
return mdList.getUsername()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPassword(): String {
|
override fun getPassword(): String {
|
||||||
return trackManager.mdList.getPassword()
|
return mdList.getPassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun login(
|
override suspend fun login(
|
||||||
@@ -198,111 +204,69 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
password: String,
|
password: String,
|
||||||
twoFactorCode: String?
|
twoFactorCode: String?
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return withIOContext {
|
val result = loginHelper.login(username, password)
|
||||||
val formBody = FormBody.Builder().apply {
|
return if (result is MangaDexLoginHelper.LoginResult.Success) {
|
||||||
add("login_username", username)
|
MdUtil.updateLoginToken(result.token, preferences, mdList)
|
||||||
add("login_password", password)
|
mdList.saveCredentials(username, password)
|
||||||
add("no_js", "1")
|
true
|
||||||
add("remember_me", "1")
|
} else false
|
||||||
add("two_factor", twoFactorCode ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
client.newCall(
|
|
||||||
POST(
|
|
||||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
|
||||||
headers,
|
|
||||||
formBody.build()
|
|
||||||
)
|
|
||||||
).await().closeQuietly()
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
|
|
||||||
|
|
||||||
withIOContext { response.body?.string() }.let { jsonData ->
|
|
||||||
if (jsonData != null) {
|
|
||||||
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
|
|
||||||
} else {
|
|
||||||
throw Exception("Json data was null")
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
preferences.setTrackCredentials(trackManager.mdList, username, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout(): Boolean {
|
override suspend fun logout(): Boolean {
|
||||||
return withIOContext {
|
val result = try {
|
||||||
// https://mangadex.org/ajax/actions.ajax.php?function=logout
|
loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList))
|
||||||
val httpUrl = MdUtil.baseUrl.toHttpUrl()
|
} catch (e: NoSessionException) {
|
||||||
val listOfDexCookies = network.cookieManager.get(httpUrl)
|
true
|
||||||
val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
} catch (e: Exception) {
|
||||||
val token = cookie?.value
|
e.message?.equals("HTTP error 405") ?: false
|
||||||
if (token.isNullOrEmpty()) {
|
|
||||||
return@withIOContext true
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val result = client.newCall(
|
|
||||||
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
|
||||||
).await()
|
|
||||||
val resultStr = withIOContext { result.body?.string() }
|
|
||||||
if (resultStr?.contains("success", true) == true) {
|
|
||||||
network.cookieManager.remove(httpUrl)
|
|
||||||
trackManager.mdList.logout()
|
|
||||||
return@withIOContext true
|
|
||||||
}
|
|
||||||
} catch (e: EOFException) {
|
|
||||||
network.cookieManager.remove(httpUrl)
|
|
||||||
trackManager.mdList.logout()
|
|
||||||
return@withIOContext true
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return if (result) {
|
||||||
|
mdList.logout()
|
||||||
|
true
|
||||||
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> {
|
override suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
|
||||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchAllFollows(forceHd) }
|
return followsHandler.fetchAllFollows()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateReadingProgress(track: Track): Boolean {
|
suspend fun updateReadingProgress(track: Track): Boolean {
|
||||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateReadingProgress(track) }
|
return followsHandler.updateReadingProgress(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateRating(track: Track): Boolean {
|
suspend fun updateRating(track: Track): Boolean {
|
||||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateRating(track) }
|
return followsHandler.updateRating(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchTrackingInfo(url: String): Track {
|
override suspend fun fetchTrackingInfo(url: String): Track {
|
||||||
return withIOContext {
|
if (!isLogged()) {
|
||||||
if (!isLogged()) {
|
throw Exception("Not Logged in")
|
||||||
throw Exception("Not Logged in")
|
|
||||||
}
|
|
||||||
FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchTrackingInfo(url)
|
|
||||||
}
|
}
|
||||||
|
return followsHandler.fetchTrackingInfo(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
|
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata?> {
|
||||||
return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail())
|
return mangaHandler.getTrackingInfo(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateFollowStatus(mangaID, followStatus) }
|
return followsHandler.updateFollowStatus(mangaID, followStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterHeader(controller: BaseController<*>): MangaDexFabHeaderAdapter {
|
override fun getFilterHeader(controller: BaseController<*>, onClick: () -> Unit): MangaDexFabHeaderAdapter {
|
||||||
return MangaDexFabHeaderAdapter(controller, this)
|
return MangaDexFabHeaderAdapter(controller, this, onClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchRandomMangaUrl(): String {
|
override suspend fun fetchRandomMangaUrl(): String {
|
||||||
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
|
return mangaHandler.fetchRandomMangaId()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
|
suspend fun getMangaSimilar(manga: MangaInfo): MangasPage {
|
||||||
return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
|
return similarHandler.getSimilar(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
/*private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
||||||
when {
|
when {
|
||||||
query.toIntOrNull() != null -> {
|
query.toIntOrNull() != null -> {
|
||||||
runAsObservable({
|
runAsObservable({
|
||||||
@@ -320,11 +284,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> fail()
|
else -> fail()
|
||||||
}
|
}*/
|
||||||
|
|
||||||
companion object {
|
/*companion object {
|
||||||
private const val REMEMBER_ME = "mangadex_rememberme_token"
|
private const val REMEMBER_ME = "mangadex_rememberme_token"
|
||||||
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
|
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
|
||||||
private const val LOW_QUALITY = 1
|
private const val LOW_QUALITY = 1
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ import exh.log.xLogW
|
|||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.util.executeOnIO
|
import exh.util.executeOnIO
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
@@ -142,33 +146,44 @@ class MergedSource : HttpSource() {
|
|||||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
||||||
|
|
||||||
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
||||||
return mangaReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.map {
|
var exception: Exception? = null
|
||||||
it.load(db, sourceManager)
|
return supervisorScope {
|
||||||
}.mapNotNull { loadedManga ->
|
mangaReferences
|
||||||
withIOContext {
|
.map {
|
||||||
if (loadedManga.manga != null && loadedManga.reference.getChapterUpdates) {
|
async {
|
||||||
loadedManga.source.getChapterList(loadedManga.manga.toMangaInfo())
|
try {
|
||||||
.map { it.toSChapter() }
|
if (it.mangaSourceId == MERGED_SOURCE_ID) return@async null
|
||||||
.let { syncChaptersWithSource(db, it, loadedManga.manga, loadedManga.source) }
|
val (source, loadedManga, reference) =
|
||||||
.also {
|
it.load(db, sourceManager)
|
||||||
if (ifDownloadNewChapters && loadedManga.reference.downloadChapters) {
|
if (loadedManga != null && reference.getChapterUpdates) {
|
||||||
downloadManager.downloadChapters(loadedManga.manga, it.first)
|
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
||||||
|
.map { it.toSChapter() }
|
||||||
|
val results =
|
||||||
|
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
||||||
|
if (ifDownloadNewChapters && reference.downloadChapters) {
|
||||||
|
downloadManager.downloadChapters(
|
||||||
|
loadedManga,
|
||||||
|
results.first
|
||||||
|
)
|
||||||
|
}
|
||||||
|
results
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
exception = e
|
||||||
|
null
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
null
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.let { pairs ->
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception!!
|
||||||
|
}
|
||||||
|
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}.let { pairs ->
|
|
||||||
val firsts = mutableListOf<Chapter>()
|
|
||||||
val seconds = mutableListOf<Chapter>()
|
|
||||||
|
|
||||||
pairs.forEach {
|
|
||||||
firsts.addAll(it.first)
|
|
||||||
seconds.addAll(it.second)
|
|
||||||
}
|
|
||||||
|
|
||||||
firsts to seconds
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -26,7 +23,6 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
class NHentai(delegate: HttpSource, val context: Context) :
|
class NHentai(delegate: HttpSource, val context: Context) :
|
||||||
@@ -48,32 +44,18 @@ class NHentai(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it).andThen(
|
|
||||||
Observable.just(
|
|
||||||
manga.apply {
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response)
|
return parseToManga(manga, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
|
override suspend fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
|
||||||
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(
|
val json = GALLERY_JSON_REGEX.find(input.body?.string().orEmpty())!!.groupValues[1].replace(
|
||||||
UNICODE_ESCAPE_REGEX
|
UNICODE_ESCAPE_REGEX
|
||||||
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
|
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
|
||||||
val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json)
|
val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json)
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -23,7 +20,6 @@ import exh.util.urlImportFetchSearchManga
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
class PervEden(delegate: HttpSource, val context: Context) :
|
class PervEden(delegate: HttpSource, val context: Context) :
|
||||||
@@ -34,25 +30,17 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
|||||||
override val lang = delegate.lang
|
override val lang = delegate.lang
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response.asJsoup())
|
return parseToManga(manga, response.asJsoup())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
url = input.location().toUri().path
|
url = input.location().toUri().path
|
||||||
|
|
||||||
@@ -137,7 +125,7 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
override suspend fun mapUrlToMangaUrl(uri: Uri): String {
|
||||||
val newUri = "http://www.perveden.com/".toUri().buildUpon()
|
val newUri = "http://www.perveden.com/".toUri().buildUpon()
|
||||||
uri.pathSegments.take(3).forEach {
|
uri.pathSegments.take(3).forEach {
|
||||||
newUri.appendPath(it)
|
newUri.appendPath(it)
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package eu.kanade.tachiyomi.source.online.english
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -22,7 +19,6 @@ import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
|||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
class EightMuses(delegate: HttpSource, val context: Context) :
|
class EightMuses(delegate: HttpSource, val context: Context) :
|
||||||
@@ -34,19 +30,11 @@ class EightMuses(delegate: HttpSource, val context: Context) :
|
|||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response.asJsoup())
|
return parseToManga(manga, response.asJsoup())
|
||||||
@@ -65,7 +53,7 @@ class EightMuses(delegate: HttpSource, val context: Context) :
|
|||||||
return SelfContents(selfAlbums, selfImages)
|
return SelfContents(selfAlbums, selfImages)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
path = input.location().toUri().pathSegments
|
path = input.location().toUri().pathSegments
|
||||||
|
|
||||||
@@ -101,7 +89,7 @@ class EightMuses(delegate: HttpSource, val context: Context) :
|
|||||||
"8muses.com"
|
"8muses.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
override suspend fun mapUrlToMangaUrl(uri: Uri): String {
|
||||||
var path = uri.pathSegments.drop(2)
|
var path = uri.pathSegments.drop(2)
|
||||||
if (uri.pathSegments[1].toLowerCase() == "picture") {
|
if (uri.pathSegments[1].toLowerCase() == "picture") {
|
||||||
path = path.dropLast(1)
|
path = path.dropLast(1)
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ package eu.kanade.tachiyomi.source.online.english
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -21,7 +18,6 @@ import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
|
|||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
class HBrowse(delegate: HttpSource, val context: Context) :
|
class HBrowse(delegate: HttpSource, val context: Context) :
|
||||||
@@ -33,25 +29,17 @@ class HBrowse(delegate: HttpSource, val context: Context) :
|
|||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(context, query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response.asJsoup())
|
return parseToManga(manga, response.asJsoup())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
|
||||||
val tables = parseIntoTables(input)
|
val tables = parseIntoTables(input)
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
|
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
|
||||||
@@ -92,7 +80,7 @@ class HBrowse(delegate: HttpSource, val context: Context) :
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
return "/${uri.pathSegments.first()}/c00001/"
|
return uri.pathSegments.firstOrNull()?.let { "/$it/c00001/" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ package eu.kanade.tachiyomi.source.online.english
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -54,26 +52,17 @@ class Pururin(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup())
|
|
||||||
.andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response.asJsoup())
|
return parseToManga(manga, response.asJsoup())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) {
|
||||||
val selfLink = input.select("[itemprop=name]").last().parent()
|
val selfLink = input.select("[itemprop=name]").last().parent()
|
||||||
val parsedSelfLink = selfLink.attr("href").toUri().pathSegments
|
val parsedSelfLink = selfLink.attr("href").toUri().pathSegments
|
||||||
|
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
prId = parsedSelfLink[parsedSelfLink.lastIndex - 1].toIntOrNull()
|
prId = parsedSelfLink[parsedSelfLink.lastIndex - 1].toInt()
|
||||||
prShortLink = parsedSelfLink.last()
|
prShortLink = parsedSelfLink.last()
|
||||||
|
|
||||||
val contentWrapper = input.selectFirst(".content-wrapper")
|
val contentWrapper = input.selectFirst(".content-wrapper")
|
||||||
@@ -123,7 +112,7 @@ class Pururin(delegate: HttpSource, val context: Context) :
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String {
|
override suspend fun mapUrlToMangaUrl(uri: Uri): String {
|
||||||
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
|
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments.getOrNull(1)}/${uri.lastPathSegment}"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter {
|
override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter {
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package eu.kanade.tachiyomi.source.online.english
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
@@ -51,20 +49,12 @@ class Tsumino(delegate: HttpSource, val context: Context) :
|
|||||||
return "https://tsumino.com/Book/Info/${uri.lastPathSegment}"
|
return "https://tsumino.com/Book/Info/${uri.lastPathSegment}"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
return parseToManga(manga, response.asJsoup())
|
return parseToManga(manga, response.asJsoup())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
|
override suspend fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location())!!.toInt()
|
tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location())!!.toInt()
|
||||||
tags.clear()
|
tags.clear()
|
||||||
|
|||||||
@@ -25,15 +25,18 @@ abstract class BaseThemedActivity : AppCompatActivity() {
|
|||||||
when (preferences.themeDark().get()) {
|
when (preferences.themeDark().get()) {
|
||||||
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
|
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
|
||||||
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
|
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
|
||||||
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Dark_Amoled
|
DarkThemeVariant.greenapple -> R.style.Theme_Tachiyomi_Dark_GreenApple
|
||||||
DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Dark_Red
|
|
||||||
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
|
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
|
||||||
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Dark_HotPink
|
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
||||||
|
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Amoled_HotPink
|
||||||
|
DarkThemeVariant.amoledblue -> R.style.Theme_Tachiyomi_Amoled_Blue
|
||||||
|
DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Amoled_Red
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
when (preferences.themeLight().get()) {
|
when (preferences.themeLight().get()) {
|
||||||
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
|
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
|
||||||
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
|
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
|
||||||
|
LightThemeVariant.strawberrydaiquiri -> R.style.Theme_Tachiyomi_Light_StrawberryDaiquiri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTheme(themeId)
|
setTheme(themeId)
|
||||||
|
|||||||
+2
-1
@@ -29,8 +29,9 @@ class OneWayFadeChangeHandler : FadeChangeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (from != null && (!isPush || removesFromViewOnPush())) {
|
if (from != null && (!isPush || removesFromViewOnPush())) {
|
||||||
container.removeView(from)
|
from.alpha = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
return animator
|
return animator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.base.controller
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
@@ -22,11 +21,9 @@ fun Router.popControllerWithTag(tag: String): Boolean {
|
|||||||
|
|
||||||
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
|
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
permissions.forEach { permission ->
|
||||||
permissions.forEach { permission ->
|
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
||||||
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
requestPermissions(arrayOf(permission), requestCode)
|
||||||
requestPermissions(arrayOf(permission), requestCode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ open class ExtensionController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.extension_main, menu)
|
inflater.inflate(R.menu.browse_extensions, menu)
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
val searchView = searchItem.actionView as SearchView
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import coil.clear
|
||||||
|
import coil.load
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
@@ -38,8 +39,8 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
binding.lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
binding.lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
||||||
binding.warning.text = when {
|
binding.warning.text = when {
|
||||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
||||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
|
||||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||||
|
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||||
// SY -->
|
// SY -->
|
||||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
|
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
|
||||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short).plusRepo(extension)
|
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short).plusRepo(extension)
|
||||||
@@ -47,11 +48,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
// SY <--
|
// SY <--
|
||||||
}.toUpperCase()
|
}.toUpperCase()
|
||||||
|
|
||||||
GlideApp.with(itemView.context).clear(binding.image)
|
binding.image.clear()
|
||||||
if (extension is Extension.Available) {
|
if (extension is Extension.Available) {
|
||||||
GlideApp.with(itemView.context)
|
binding.image.load(extension.iconUrl)
|
||||||
.load(extension.iconUrl)
|
|
||||||
.into(binding.image)
|
|
||||||
} else {
|
} else {
|
||||||
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
|
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-7
@@ -21,7 +21,6 @@ import androidx.preference.PreferenceManager
|
|||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
|
||||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||||
import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
@@ -67,7 +66,10 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
val themedContext by lazy { getPreferenceThemeContext() }
|
val themedContext by lazy { getPreferenceThemeContext() }
|
||||||
val manager = PreferenceManager(themedContext)
|
val manager = PreferenceManager(themedContext)
|
||||||
manager.preferenceDataStore = EmptyPreferenceDataStore()
|
val dataStore = SharedPreferencesDataStore(
|
||||||
|
context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||||
|
)
|
||||||
|
manager.preferenceDataStore = dataStore
|
||||||
manager.onDisplayPreferenceDialogListener = this
|
manager.onDisplayPreferenceDialogListener = this
|
||||||
val screen = manager.createPreferenceScreen(themedContext)
|
val screen = manager.createPreferenceScreen(themedContext)
|
||||||
preferenceScreen = screen
|
preferenceScreen = screen
|
||||||
@@ -112,10 +114,6 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
|||||||
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
|
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
|
||||||
val context = screen.context
|
val context = screen.context
|
||||||
|
|
||||||
val dataStore = SharedPreferencesDataStore(
|
|
||||||
context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (source is ConfigurableSource) {
|
if (source is ConfigurableSource) {
|
||||||
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
||||||
source.setupPreferenceScreen(newScreen)
|
source.setupPreferenceScreen(newScreen)
|
||||||
@@ -124,7 +122,6 @@ class SourcePreferencesController(bundle: Bundle? = null) :
|
|||||||
while (newScreen.preferenceCount != 0) {
|
while (newScreen.preferenceCount != 0) {
|
||||||
val pref = newScreen.getPreference(0)
|
val pref = newScreen.getPreference(0)
|
||||||
pref.isIconSpaceReserved = false
|
pref.isIconSpaceReserved = false
|
||||||
pref.preferenceDataStore = dataStore
|
|
||||||
pref.order = Int.MAX_VALUE // reset to default order
|
pref.order = Int.MAX_VALUE // reset to default order
|
||||||
|
|
||||||
newScreen.removePreference(pref)
|
newScreen.removePreference(pref)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.latest
|
package eu.kanade.tachiyomi.ui.browse.latest
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import coil.clear
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.transition.CrossfadeTransition
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
|
||||||
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
|
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
|
||||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||||
|
|
||||||
@@ -42,15 +45,18 @@ class LatestCardHolder(view: View, adapter: LatestCardAdapter) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setImage(manga: Manga) {
|
fun setImage(manga: Manga) {
|
||||||
GlideApp.with(itemView.context).clear(binding.cover)
|
binding.cover.clear()
|
||||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||||
GlideApp.with(itemView.context)
|
val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
|
||||||
.load(manga.toMangaThumbnail())
|
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
}
|
||||||
.centerCrop()
|
val request = ImageRequest.Builder(itemView.context)
|
||||||
.skipMemoryCache(true)
|
.data(manga)
|
||||||
.placeholder(android.R.color.transparent)
|
.setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||||
.into(StateImageViewTarget(binding.cover, binding.progress))
|
.diskCachePolicy(CachePolicy.DISABLED)
|
||||||
|
.target(StateImageViewTarget(binding.cover, binding.progress, crossfadeDuration))
|
||||||
|
.build()
|
||||||
|
itemView.context.imageLoader.enqueue(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-9
@@ -1,10 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.RadioButton
|
import android.widget.RadioButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -22,14 +22,11 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
class MigrationBottomSheetDialog(activity: Activity, private val listener: StartMigrationListener) : BaseBottomSheetDialog(activity) {
|
class MigrationBottomSheetDialog(activity: Activity, private val listener: StartMigrationListener) : BaseBottomSheetDialog(activity) {
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val binding: MigrationBottomSheetBinding = MigrationBottomSheetBinding.inflate(activity.layoutInflater)
|
lateinit var binding: MigrationBottomSheetBinding
|
||||||
|
|
||||||
init {
|
override fun createView(inflater: LayoutInflater): View {
|
||||||
setContentView(binding.root)
|
binding = MigrationBottomSheetBinding.inflate(inflater)
|
||||||
if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
return binding.root
|
||||||
binding.sourceGroup.orientation = LinearLayout.HORIZONTAL
|
|
||||||
}
|
|
||||||
window?.setBackgroundDrawable(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -272,7 +272,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
override fun updateCount() {
|
override fun updateCount() {
|
||||||
launchUI {
|
launchUI {
|
||||||
if (router.backstack.last().controller() == this@MigrationListController) {
|
if (router.backstack.lastOrNull()?.controller() == this@MigrationListController) {
|
||||||
setTitle()
|
setTitle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user