Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9583a31c9 | |||
| 926fa85ccd | |||
| b91252df67 | |||
| 3893c90eb2 | |||
| d5f4783aca | |||
| b0bcfa9db0 | |||
| 01ea86ab90 | |||
| 475299d9b3 | |||
| 951bb1f3c6 | |||
| 1f7e69e13c | |||
| 5fbaa7d6be | |||
| cce1b135c9 | |||
| b344a3944e | |||
| 7f416bda7c | |||
| 3b08c7fdea | |||
| e346d95b0e | |||
| 0fe8990f99 | |||
| 35ed8e2d34 | |||
| 12d01b9da3 | |||
| 2b7ffc8ba2 | |||
| 1b91062767 | |||
| 4a71eb2ff0 | |||
| 6fe9284c07 | |||
| 51c2a1b048 | |||
| 03b3046ece | |||
| ea2f050f86 | |||
| f41077449a | |||
| aad0ac7296 | |||
| 5e59d05598 | |||
| 337d270d2a | |||
| 09c9e15281 | |||
| 057ccf74ce | |||
| c21771823c | |||
| 987e5bcf33 | |||
| 3920a5a73b | |||
| 00701aeda1 | |||
| bb47188d5c | |||
| 06e57b790e | |||
| ff48e89161 | |||
| e338bb0f47 | |||
| ae48c1d7d4 | |||
| 243c65d012 | |||
| e9903a6678 | |||
| afe32f1099 | |||
| acf2ad7c77 | |||
| 4286fd606a | |||
| 70d134b375 | |||
| a8d0564eb0 | |||
| df6bbbd4c6 | |||
| 7a08fa3398 | |||
| 46d9c024da | |||
| 2c49466a42 | |||
| a6cba5c87d | |||
| 032504f128 | |||
| f5b6fc5b54 | |||
| c0e1ca1185 | |||
| fa812830b8 | |||
| b4c68f454d | |||
| a13166b69d | |||
| 0556c5c2ff | |||
| c449a59696 | |||
| 2339388d6f | |||
| 9c669d040a | |||
| 82acb4412a | |||
| 23b0c3305d | |||
| 47373a9483 | |||
| 87e3a610e1 | |||
| 94d14af2a4 | |||
| 99becd4fd6 | |||
| f21ef47c87 | |||
| 2ef7212128 | |||
| 0003d11da3 | |||
| bf49023693 | |||
| e1bdb1dd0f | |||
| 2222c030b8 | |||
| 1631bfd5c6 | |||
| 72f3ebb70d | |||
| 17e5ebd171 | |||
| b2059288b7 | |||
| f24926fc81 | |||
| aba324a461 | |||
| 4a19f8cff2 | |||
| 302db11482 | |||
| 1af2698b72 | |||
| 3e9c8dbfd2 | |||
| a38cb2ab5f | |||
| 589464d723 | |||
| d7f3b399f4 | |||
| 646aeb66c5 | |||
| 135f0bdd95 | |||
| 80394dab4a | |||
| 75e9911317 | |||
| a311a3b497 | |||
| f3b6855684 | |||
| 2ee69c2ac4 | |||
| ff0516726b | |||
| 7e5de79d5f | |||
| dabb7a0494 | |||
| ff1e0d7578 | |||
| 4771fa529d | |||
| 8e94afb9c1 | |||
| 570db67894 | |||
| 875b2fbccd | |||
| e562f0392d | |||
| e142af00fa | |||
| a5c4098109 | |||
| 62091790a5 | |||
| 7ddfedd9c7 | |||
| 70a779e4d0 | |||
| fd40f35371 | |||
| f66aff9ed7 | |||
| 29ad0e091f | |||
| fa580aa3c9 | |||
| bd8bc3a3cb | |||
| 52f2644035 | |||
| 7a97d6f20d | |||
| 8de67c49bc | |||
| 7530a7bd4e | |||
| 1ac7043163 | |||
| 8e24797e50 | |||
| 4513af8425 | |||
| 78d1a6cecb | |||
| a9e9fe59c6 | |||
| a8f2f03562 | |||
| 9e986bbeb6 | |||
| b904bf99e8 | |||
| 8b95d93a96 | |||
| 74012e0830 | |||
| 362f0a6671 | |||
| 0ca87a3763 | |||
| 840ab68922 | |||
| 4663d64c05 | |||
| 4b7c33be16 | |||
| 8c40e4d635 | |||
| eaae98d072 | |||
| 5ffc21fc9e | |||
| 294caa25a4 | |||
| 923f5213cd | |||
| 8434b880c6 | |||
| badd43046b | |||
| 00d5fd8fe4 | |||
| 3bd6b8524f | |||
| 362ba1bf69 | |||
| 450b76f495 | |||
| 1188ee10d8 | |||
| 372e570fac | |||
| 8ab2a823b5 | |||
| 7fb197a752 | |||
| 7046d304e0 | |||
| dfa4eda33b | |||
| a229d015ad | |||
| eacdf4e161 | |||
| 9569f13190 | |||
| 02ba0eca32 | |||
| e3d2e5b89d | |||
| a9317dff88 | |||
| c2c2a3be01 | |||
| 57565fce2d | |||
| 439b78c39f | |||
| fbb14a35a9 | |||
| c0a4f4e93a | |||
| c543622268 | |||
| 43034db5e5 | |||
| 27ad39b6ce | |||
| 04749a8fce | |||
| 15b23e35cd | |||
| a839372d9f | |||
| e1fd0d1a4a | |||
| 6469121f41 | |||
| b8129ff4f6 | |||
| 201356afeb | |||
| 2e033356aa | |||
| 044c638079 | |||
| bbf1c4ffd9 |
@@ -14,9 +14,9 @@
|
|||||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||||
|
|
||||||
# Bugs
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (More > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Dev version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels: "feature"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ Features of TachiyomiSY include:
|
|||||||
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
||||||
* Comfortable grid view
|
* Comfortable grid view
|
||||||
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
|
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
|
||||||
|
* Manga info edit
|
||||||
|
* Enhanced views for internal and integrated sources
|
||||||
|
* Enhanced usability for internal and delegated sources
|
||||||
|
|
||||||
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
||||||
* Source migration, migrate all your manga from one source to another
|
* Source migration, migrate all your manga from one source to another
|
||||||
@@ -42,12 +45,12 @@ Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified
|
|||||||
* * nHentai
|
* * nHentai
|
||||||
* * Hitomi.la
|
* * Hitomi.la
|
||||||
* * 8Muses
|
* * 8Muses
|
||||||
* * HBrowse
|
|
||||||
* * Perv Eden
|
* * Perv Eden
|
||||||
* Additional features for some extensions, features include custom description, opening in app, batch add to library:
|
* Additional features for some extensions, features include custom description, opening in app, batch add to library:
|
||||||
* * Puruin
|
* * Puruin
|
||||||
* * Tsumino
|
* * Tsumino
|
||||||
* * HentaiCafe (Foolside)
|
* * HentaiCafe (Foolside)
|
||||||
|
* * HBrowse
|
||||||
* Saving searches
|
* Saving searches
|
||||||
* Autoscroll
|
* Autoscroll
|
||||||
* Page preload customization
|
* Page preload customization
|
||||||
@@ -81,7 +84,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Bugs</summary>
|
<details><summary>Bugs</summary>
|
||||||
|
|
||||||
* Include version (Setting > About > Version)
|
* Include version (More > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
|
|||||||
+26
-19
@@ -36,15 +36,14 @@ ext {
|
|||||||
android {
|
android {
|
||||||
compileSdkVersion AndroidConfig.compileSdk
|
compileSdkVersion AndroidConfig.compileSdk
|
||||||
buildToolsVersion AndroidConfig.buildTools
|
buildToolsVersion AndroidConfig.buildTools
|
||||||
publishNonDefault true
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi.sy"
|
applicationId "eu.kanade.tachiyomi.sy"
|
||||||
minSdkVersion AndroidConfig.minSdk
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion AndroidConfig.targetSdk
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 2
|
versionCode 4
|
||||||
versionName "1.0.0"
|
versionName "1.1.1"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@@ -129,7 +128,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,17 +138,13 @@ androidExtensions {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
|
||||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
|
||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0-alpha01'
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
@@ -168,9 +163,9 @@ dependencies {
|
|||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
// UI library
|
// UI library
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha01'
|
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||||
|
|
||||||
standardImplementation 'com.google.firebase:firebase-core:17.4.3'
|
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
@@ -204,6 +199,7 @@ dependencies {
|
|||||||
// Disk
|
// Disk
|
||||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
@@ -221,7 +217,7 @@ dependencies {
|
|||||||
implementation 'io.requery:sqlite-android:3.31.0'
|
implementation 'io.requery:sqlite-android:3.31.0'
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
@@ -237,12 +233,14 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
|
||||||
|
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
final acra_version = '5.5.0'
|
//final acra_version = '5.5.0'
|
||||||
implementation "ch.acra:acra-http:$acra_version"
|
//implementation "ch.acra:acra-http:$acra_version"
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
@@ -272,7 +270,7 @@ dependencies {
|
|||||||
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
final flowbinding_version = '0.11.1'
|
final flowbinding_version = '0.12.0'
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||||
@@ -297,14 +295,20 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
|
||||||
final coroutines_version = '1.3.7'
|
|
||||||
|
final coroutines_version = '1.3.8'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
||||||
|
|
||||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
|
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||||
|
|
||||||
|
// Debug tool; see https://fbflipper.com/
|
||||||
|
// debugImplementation 'com.facebook.flipper:flipper:0.49.0'
|
||||||
|
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||||
|
|
||||||
// Text distance (EH)
|
// Text distance (EH)
|
||||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||||
|
|
||||||
@@ -338,6 +342,9 @@ dependencies {
|
|||||||
// Humanize (EH)
|
// Humanize (EH)
|
||||||
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
||||||
|
|
||||||
|
// RatingBar (SY)
|
||||||
|
implementation 'me.zhanghai.android.materialratingbar:library:1.3.1'
|
||||||
|
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
|
|
||||||
final def markwon_version = '4.1.0'
|
final def markwon_version = '4.1.0'
|
||||||
@@ -380,7 +387,7 @@ task copyResources(type: Copy) {
|
|||||||
|
|
||||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
preBuild.dependsOn(ktlintFormat, copyResources)
|
||||||
|
|
||||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
apply plugin: 'io.fabric'
|
apply plugin: 'io.fabric'
|
||||||
|
|||||||
@@ -39,11 +39,6 @@
|
|||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.main.ForceCloseActivity"
|
|
||||||
android:clearTaskOnLaunch="true"
|
|
||||||
android:noHistory="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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.widget.Toast
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
@@ -29,11 +28,8 @@ import com.ms_square.debugoverlay.DebugOverlay
|
|||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.main.ForceCloseActivity
|
|
||||||
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.WebViewUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
@@ -63,11 +59,14 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
workaroundAndroid7BrokenSSL()
|
workaroundAndroid7BrokenSSL()
|
||||||
|
|
||||||
// Enforce WebView availability
|
// Debug tool; see https://fbflipper.com/
|
||||||
if (!WebViewUtil.supportsWebView(this)) {
|
// SoLoader.init(this, false)
|
||||||
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
||||||
ForceCloseActivity.closeApp(this)
|
// val client = AndroidFlipperClient.getInstance(this)
|
||||||
}
|
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||||
|
// client.addPlugin(DatabasesFlipperPlugin(this))
|
||||||
|
// client.start()
|
||||||
|
// }
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
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.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
@@ -42,6 +43,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { DownloadManager(app) }
|
addSingletonFactory { DownloadManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { CustomMangaManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
addSingletonFactory { Gson() }
|
||||||
@@ -63,5 +66,9 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
GlobalScope.launch { get<DatabaseHelper>() }
|
GlobalScope.launch { get<DatabaseHelper>() }
|
||||||
|
|
||||||
GlobalScope.launch { get<DownloadManager>() }
|
GlobalScope.launch { get<DownloadManager>() }
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
GlobalScope.launch { get<CustomMangaManager>() }
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
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
|
||||||
@@ -106,7 +107,7 @@ class BackupCreateService : Service() {
|
|||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
backupManager = BackupManager(this)
|
backupManager = BackupManager(this)
|
||||||
|
|
||||||
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
|
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile)
|
notifier.showBackupComplete(unifile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
@@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val backupManager = BackupManager(context)
|
val backupManager = BackupManager(context)
|
||||||
val uri = Uri.parse(preferences.backupsDirectory().get())
|
val uri = preferences.backupsDirectory().get().toUri()
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
backupManager.createBackup(uri, flags, true)
|
backupManager.createBackup(uri, flags, true)
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
// If the category is already in the db, assign the id to the file's category
|
// If the category is already in the db, assign the id to the file's category
|
||||||
// and do nothing
|
// and do nothing
|
||||||
if (category.nameLower == dbCategory.nameLower) {
|
if (category.name == dbCategory.name) {
|
||||||
category.id = dbCategory.id
|
category.id = dbCategory.id
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -387,7 +387,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
if (backupCategoryStr == dbCategory.name) {
|
||||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val throttleManager = EHentaiThrottleManager()
|
private val throttleManager = EHentaiThrottleManager()
|
||||||
|
|
||||||
|
private var skippedAmount = 0
|
||||||
|
|
||||||
|
private var totalAmount = 0
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,12 +121,6 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private var restoreAmount = 0
|
private var restoreAmount = 0
|
||||||
|
|
||||||
// SY -->
|
|
||||||
private var skippedAmount = 0
|
|
||||||
|
|
||||||
private var totalAmount = 0
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of source ID to source name from backup data
|
* Mapping of source ID to source name from backup data
|
||||||
*/
|
*/
|
||||||
@@ -288,7 +286,7 @@ class BackupRestoreService : Service() {
|
|||||||
backupManager.restoreSavedSearches(savedSearchesJson)
|
backupManager.restoreSavedSearches(savedSearchesJson)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.eh_saved_searches))
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -320,13 +318,8 @@ class BackupRestoreService : Service() {
|
|||||||
if (source != null) {
|
if (source != null) {
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
} else {
|
} else {
|
||||||
val message = if (manga.source in sourceMapping) {
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
getString(R.string.source_not_found_name, sourceMapping[manga.source])
|
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
||||||
} else {
|
|
||||||
getString(R.string.source_not_found)
|
|
||||||
}
|
|
||||||
|
|
||||||
errors.add(Date() to "${manga.title} - $message")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ object MangaTypeAdapter {
|
|||||||
write {
|
write {
|
||||||
beginArray()
|
beginArray()
|
||||||
value(it.url)
|
value(it.url)
|
||||||
value(it.title)
|
// SY -->
|
||||||
|
value(it.originalTitle)
|
||||||
|
// SY <--
|
||||||
value(it.source)
|
value(it.source)
|
||||||
value(it.viewer)
|
value(it.viewer)
|
||||||
value(it.chapter_flags)
|
value(it.chapter_flags)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = /* SY --> */ 2 /* SY <-- */
|
const val DATABASE_VERSION = /* SY --> */ 3 /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -66,6 +66,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
db.execSQL(MangaTable.addCoverLastModified)
|
db.execSQL(MangaTable.addCoverLastModified)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 3) {
|
||||||
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
||||||
|
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_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
@@ -47,15 +48,17 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
put(COL_SOURCE, obj.source)
|
put(COL_SOURCE, obj.source)
|
||||||
put(COL_URL, obj.url)
|
put(COL_URL, obj.url)
|
||||||
put(COL_ARTIST, obj.artist)
|
// SY -->
|
||||||
put(COL_AUTHOR, obj.author)
|
put(COL_ARTIST, obj.originalArtist)
|
||||||
put(COL_DESCRIPTION, obj.description)
|
put(COL_AUTHOR, obj.originalAuthor)
|
||||||
put(COL_GENRE, obj.genre)
|
put(COL_DESCRIPTION, obj.originalDescription)
|
||||||
put(COL_TITLE, obj.title)
|
put(COL_GENRE, obj.originalGenre)
|
||||||
|
put(COL_TITLE, obj.originalTitle)
|
||||||
|
// SY <--
|
||||||
put(COL_STATUS, obj.status)
|
put(COL_STATUS, obj.status)
|
||||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||||
put(COL_FAVORITE, obj.favorite)
|
put(COL_FAVORITE, obj.favorite)
|
||||||
@@ -64,6 +67,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
put(COL_VIEWER, obj.viewer)
|
put(COL_VIEWER, obj.viewer)
|
||||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||||
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||||
|
put(COL_DATE_ADDED, obj.date_added)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +89,7 @@ interface BaseMangaGetResolver {
|
|||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ interface Category : Serializable {
|
|||||||
var mangaOrder: List<Long>
|
var mangaOrder: List<Long>
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val nameLower: String
|
|
||||||
get() = name.toLowerCase()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class CategoryImpl : Category {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val category = other as Category
|
val category = other as Category
|
||||||
|
|
||||||
return name == category.name
|
return name == category.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val chapter = other as Chapter
|
val chapter = other as Chapter
|
||||||
return url == chapter.url
|
if (url != chapter.url) return false
|
||||||
|
return id == chapter.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode() + id.hashCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
var date_added: Long
|
||||||
|
|
||||||
var viewer: Int
|
var viewer: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
@@ -22,10 +24,6 @@ interface Manga : SManga {
|
|||||||
setFlags(order, SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
return chapter_flags and SORT_MASK == SORT_DESC
|
||||||
}
|
}
|
||||||
@@ -34,6 +32,10 @@ interface Manga : SManga {
|
|||||||
return genre?.split(", ")?.map { it.trim() }
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
chapter_flags = chapter_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 DISPLAY_MASK
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
open class MangaImpl : Manga {
|
open class MangaImpl : Manga {
|
||||||
|
|
||||||
override var id: Long? = null
|
override var id: Long? = null
|
||||||
@@ -9,17 +12,36 @@ open class MangaImpl : Manga {
|
|||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
override var title: String = ""
|
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
|
||||||
|
override var title: String
|
||||||
|
get() = if (favorite) {
|
||||||
|
val customTitle = customMangaManager.getManga(this)?.title
|
||||||
|
if (customTitle.isNullOrBlank()) ogTitle else customTitle
|
||||||
|
} else {
|
||||||
|
ogTitle
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
ogTitle = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override var author: String?
|
||||||
|
get() = if (favorite) customMangaManager.getManga(this)?.author ?: ogAuthor else ogAuthor
|
||||||
|
set(value) { ogAuthor = value }
|
||||||
|
|
||||||
|
override var artist: String?
|
||||||
|
get() = if (favorite) customMangaManager.getManga(this)?.artist ?: ogArtist else ogArtist
|
||||||
|
set(value) { ogArtist = value }
|
||||||
|
|
||||||
|
override var description: String?
|
||||||
|
get() = if (favorite) customMangaManager.getManga(this)?.description ?: ogDesc else ogDesc
|
||||||
|
set(value) { ogDesc = value }
|
||||||
|
|
||||||
|
override var genre: String?
|
||||||
|
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
||||||
|
set(value) { ogGenre = value }
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override var artist: String? = null
|
|
||||||
|
|
||||||
override var author: String? = null
|
|
||||||
|
|
||||||
override var description: String? = null
|
|
||||||
|
|
||||||
override var genre: String? = null
|
|
||||||
|
|
||||||
override var status: Int = 0
|
override var status: Int = 0
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
override var thumbnail_url: String? = null
|
||||||
@@ -28,6 +50,8 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var viewer: Int = 0
|
override var viewer: Int = 0
|
||||||
@@ -36,16 +60,29 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var cover_last_modified: Long = 0
|
override var cover_last_modified: Long = 0
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
lateinit var ogTitle: String
|
||||||
|
private set
|
||||||
|
var ogAuthor: String? = null
|
||||||
|
private set
|
||||||
|
var ogArtist: String? = null
|
||||||
|
private set
|
||||||
|
var ogDesc: String? = null
|
||||||
|
private set
|
||||||
|
var ogGenre: String? = null
|
||||||
|
private set
|
||||||
|
// SY <--
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val manga = other as Manga
|
val manga = other as Manga
|
||||||
|
if (url != manga.url) return false
|
||||||
return url == manga.url
|
return id == manga.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode() + id.hashCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
|
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.MangaTitlePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||||
@@ -84,6 +85,16 @@ interface MangaQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaInfo(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaInfoPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun resetMangaInfo(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaInfoPutResolver(true))
|
||||||
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ fun getRecentsQuery() =
|
|||||||
"""
|
"""
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
WHERE ${Manga.COL_FAVORITE} = 1
|
||||||
|
AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||||
|
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = if (reset) resetToContentValues(manga) else mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_TITLE, manga.originalTitle)
|
||||||
|
put(MangaTable.COL_GENRE, manga.originalGenre)
|
||||||
|
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
|
||||||
|
put(MangaTable.COL_ARTIST, manga.originalArtist)
|
||||||
|
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
val splitter = "▒ ▒∩▒"
|
||||||
|
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
||||||
|
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
||||||
|
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
||||||
|
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
||||||
|
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
// [EXH]
|
||||||
|
class MangaUrlPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_URL, manga.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
|
|
||||||
const val COL_VIEWER = "viewer"
|
const val COL_VIEWER = "viewer"
|
||||||
@@ -58,7 +60,8 @@ object MangaTable {
|
|||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$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
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
@@ -70,4 +73,17 @@ object MangaTable {
|
|||||||
|
|
||||||
val addCoverLastModified: String
|
val addCoverLastModified: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
val addDateAdded: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used with addDateAdded to populate it with the oldest chapter fetch date.
|
||||||
|
*/
|
||||||
|
val backfillDateAdded: String
|
||||||
|
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
|
||||||
|
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
|
||||||
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
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
|
||||||
@@ -59,7 +59,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private fun getDirectoryFromPreference(): UniFile {
|
private fun getDirectoryFromPreference(): UniFile {
|
||||||
val dir = preferences.downloadsDirectory().get()
|
val dir = preferences.downloadsDirectory().get()
|
||||||
return UniFile.fromUri(context, Uri.parse(dir))
|
return UniFile.fromUri(context, dir.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
|||||||
if (sourceDir != null) {
|
if (sourceDir != null) {
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||||
if (mangaDir != null) {
|
if (mangaDir != null) {
|
||||||
return provider.getChapterDirName(chapter) in mangaDir.files
|
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -122,7 +122,9 @@ class DownloadCache(
|
|||||||
* Renews the downloads cache.
|
* Renews the downloads cache.
|
||||||
*/
|
*/
|
||||||
private fun renew() {
|
private fun renew() {
|
||||||
val onlineSources = sourceManager.getOnlineSources()
|
// SY -->
|
||||||
|
val onlineSources = sourceManager.getVisibleOnlineSources()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
val sourceDirs = rootDir.dir.listFiles()
|
val sourceDirs = rootDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
@@ -191,9 +193,10 @@ class DownloadCache(
|
|||||||
fun removeChapter(chapter: Chapter, manga: Manga) {
|
fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
val sourceDir = rootDir.files[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||||
val chapterDirName = provider.getChapterDirName(chapter)
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (chapterDirName in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= chapterDirName
|
mangaDir.files -= it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +211,10 @@ class DownloadCache(
|
|||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
val sourceDir = rootDir.files[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
val chapterDirName = provider.getChapterDirName(chapter)
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (chapterDirName in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= chapterDirName
|
mangaDir.files -= it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(private val context: Context) {
|
class DownloadManager(/* SY private */ val context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sources manager.
|
* The sources manager.
|
||||||
@@ -251,16 +251,20 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param newChapter the target chapter with the new name.
|
* @param newChapter the target chapter with the new name.
|
||||||
*/
|
*/
|
||||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldName = provider.getChapterDirName(oldChapter)
|
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||||
val newName = provider.getChapterDirName(newChapter)
|
val newName = provider.getChapterDirName(newChapter)
|
||||||
val mangaDir = provider.getMangaDir(manga, source)
|
val mangaDir = provider.getMangaDir(manga, source)
|
||||||
|
|
||||||
val oldFolder = mangaDir.findFile(oldName)
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
|
val oldFolder = oldNames.asSequence()
|
||||||
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
if (oldFolder?.renameTo(newName) == true) {
|
if (oldFolder?.renameTo(newName) == true) {
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName, mangaDir, manga)
|
cache.addChapter(newName, mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Could not rename downloaded chapter: %s.", oldName)
|
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import eu.kanade.tachiyomi.util.lang.chop
|
|||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
@@ -23,16 +22,29 @@ import uy.kohesive.injekt.api.get
|
|||||||
*/
|
*/
|
||||||
internal class DownloadNotifier(private val context: Context) {
|
internal class DownloadNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
|
||||||
|
private val progressNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences by lazy { Injekt.get<PreferencesHelper>() }
|
private val completeNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_ERROR) {
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of download. Used for correct notification icon.
|
* Status of download. Used for correct notification icon.
|
||||||
*/
|
*/
|
||||||
@Volatile
|
|
||||||
private var isDownloading = false
|
private var isDownloading = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,14 +62,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*
|
*
|
||||||
* @param id the id of the notification.
|
* @param id the id of the notification.
|
||||||
*/
|
*/
|
||||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
|
private fun NotificationCompat.Builder.show(id: Int) {
|
||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear old actions if they exist.
|
* Clear old actions if they exist.
|
||||||
*/
|
*/
|
||||||
private fun clearActions() = with(notificationBuilder) {
|
private fun NotificationCompat.Builder.clearActions() {
|
||||||
if (mActions.isNotEmpty()) {
|
if (mActions.isNotEmpty()) {
|
||||||
mActions.clear()
|
mActions.clear()
|
||||||
}
|
}
|
||||||
@@ -68,7 +80,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* those can only be dismissed by the user.
|
* those can only be dismissed by the user.
|
||||||
*/
|
*/
|
||||||
fun dismiss() {
|
fun dismiss() {
|
||||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,8 +89,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param download download object containing download information.
|
* @param download download object containing download information.
|
||||||
*/
|
*/
|
||||||
fun onProgressChange(download: Download) {
|
fun onProgressChange(download: Download) {
|
||||||
// Create notification
|
with(progressNotificationBuilder) {
|
||||||
with(notificationBuilder) {
|
|
||||||
// Check if first call.
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
@@ -110,17 +121,16 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
}
|
|
||||||
|
|
||||||
// Displays the progress bar on notification
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
notificationBuilder.show()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notification when download is paused.
|
* Show notification when download is paused.
|
||||||
*/
|
*/
|
||||||
fun onDownloadPaused() {
|
fun onPaused() {
|
||||||
with(notificationBuilder) {
|
with(progressNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||||
@@ -141,22 +151,45 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
context.getString(R.string.action_cancel_all),
|
context.getString(R.string.action_cancel_all),
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification.
|
|
||||||
notificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset initial values
|
// Reset initial values
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function shows a notification to inform download tasks are done.
|
||||||
|
*/
|
||||||
|
fun onComplete() {
|
||||||
|
if (!errorThrown) {
|
||||||
|
// Create notification
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
|
setContentText(context.getString(R.string.download_notifier_download_finish))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
clearActions()
|
||||||
|
setAutoCancel(true)
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset states to default
|
||||||
|
errorThrown = false
|
||||||
|
isDownloading = false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the downloader receives a warning.
|
* Called when the downloader receives a warning.
|
||||||
*
|
*
|
||||||
* @param reason the text to show.
|
* @param reason the text to show.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(notificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
@@ -164,8 +197,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
notificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@@ -180,7 +214,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(notificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
chapter
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
@@ -191,8 +225,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
errorThrown = true
|
errorThrown = true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@@ -22,7 +23,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Preferences used to store the list of chapters to delete.
|
* Preferences used to store the list of chapters to delete.
|
||||||
*/
|
*/
|
||||||
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
private val preferences = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last added chapter, used to avoid decoding from the preference too often.
|
* Last added chapter, used to avoid decoding from the preference too often.
|
||||||
@@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
||||||
lastEntry.copy(chapters = newChapters)
|
lastEntry.copy(chapters = newChapters)
|
||||||
} else {
|
} else {
|
||||||
val existingEntry = prefs.getString(manga.id!!.toString(), null)
|
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
// Existing entry found on preferences, decode json and add the new chapter
|
// Existing entry found on preferences, decode json and add the new chapter
|
||||||
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
||||||
@@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
|
|
||||||
// Save current state
|
// Save current state
|
||||||
val json = gson.toJson(newEntry)
|
val json = gson.toJson(newEntry)
|
||||||
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
|
preferences.edit {
|
||||||
|
putString(newEntry.manga.id.toString(), json)
|
||||||
|
}
|
||||||
lastAddedEntry = newEntry
|
lastAddedEntry = newEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
||||||
val entries = decodeAll()
|
val entries = decodeAll()
|
||||||
prefs.edit().clear().apply()
|
preferences.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
lastAddedEntry = null
|
lastAddedEntry = null
|
||||||
|
|
||||||
return entries.associate { entry ->
|
return entries.associate { entry ->
|
||||||
@@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Decodes all the chapters from preferences.
|
* Decodes all the chapters from preferences.
|
||||||
*/
|
*/
|
||||||
private fun decodeAll(): List<Entry> {
|
private fun decodeAll(): List<Entry> {
|
||||||
return prefs.all.values.mapNotNull { rawEntry ->
|
return preferences.all.values.mapNotNull { rawEntry ->
|
||||||
try {
|
try {
|
||||||
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
private data class ChapterEntry(
|
private data class ChapterEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String
|
val name: String,
|
||||||
|
val scanlator: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,7 +160,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Returns a chapter entry from a chapter model.
|
* Returns a chapter entry from a chapter model.
|
||||||
*/
|
*/
|
||||||
private fun Chapter.toEntry(): ChapterEntry {
|
private fun Chapter.toEntry(): ChapterEntry {
|
||||||
return ChapterEntry(id!!, url, name)
|
return ChapterEntry(id!!, url, name, scanlator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,6 +180,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
it.id = id
|
it.id = id
|
||||||
it.url = url
|
it.url = url
|
||||||
it.name = name
|
it.name = name
|
||||||
|
it.scanlator = scanlator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
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
|
||||||
@@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
||||||
val dir = UniFile.fromUri(context, Uri.parse(it))
|
val dir = UniFile.fromUri(context, it.toUri())
|
||||||
DiskUtil.createNoMediaFile(dir, context)
|
DiskUtil.createNoMediaFile(dir, context)
|
||||||
dir
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().asFlow()
|
preferences.downloadsDirectory().asFlow()
|
||||||
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,9 @@ 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 mangaDir?.findFile(getChapterDirName(chapter))
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
|
.mapNotNull { mangaDir?.findFile(it) }
|
||||||
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,7 +102,11 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) }
|
return chapters.mapNotNull { chapter ->
|
||||||
|
getValidChapterDirNames(chapter).asSequence()
|
||||||
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,7 +124,9 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param manga the manga to query.
|
* @param manga the manga to query.
|
||||||
*/
|
*/
|
||||||
fun getMangaDirName(manga: Manga): String {
|
fun getMangaDirName(manga: Manga): String {
|
||||||
return DiskUtil.buildValidFilename(manga.title)
|
// SY -->
|
||||||
|
return DiskUtil.buildValidFilename(manga.originalTitle)
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,6 +135,25 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param chapter the chapter to query.
|
* @param chapter the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getChapterDirName(chapter: Chapter): String {
|
fun getChapterDirName(chapter: Chapter): String {
|
||||||
return DiskUtil.buildValidFilename(chapter.name)
|
return DiskUtil.buildValidFilename(
|
||||||
|
when {
|
||||||
|
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
||||||
|
else -> chapter.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns valid downloaded chapter directory names.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter to query.
|
||||||
|
*/
|
||||||
|
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||||
|
return listOf(
|
||||||
|
getChapterDirName(chapter),
|
||||||
|
|
||||||
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Notification
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkInfo.State.CONNECTED
|
import android.net.NetworkInfo.State.CONNECTED
|
||||||
import android.net.NetworkInfo.State.DISCONNECTED
|
import android.net.NetworkInfo.State.DISCONNECTED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -82,7 +83,7 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
@@ -143,7 +144,7 @@ class DownloadService : Service() {
|
|||||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||||
when (connectivity.state) {
|
when (connectivity.state) {
|
||||||
CONNECTED -> {
|
CONNECTED -> {
|
||||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||||
} else {
|
} else {
|
||||||
val started = downloadManager.startDownloads()
|
val started = downloadManager.startDownloads()
|
||||||
@@ -175,19 +176,19 @@ class DownloadService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Releases the wake lock if it's held.
|
* Releases the wake lock if it's held.
|
||||||
*/
|
*/
|
||||||
fun PowerManager.WakeLock.releaseIfNeeded() {
|
private fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||||
if (isHeld) release()
|
if (isHeld) release()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acquires the wake lock if it's not held.
|
* Acquires the wake lock if it's not held.
|
||||||
*/
|
*/
|
||||||
fun PowerManager.WakeLock.acquireIfNeeded() {
|
private fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||||
if (!isHeld) acquire()
|
if (!isHeld) acquire()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlaceholderNotification(): Notification {
|
private fun getPlaceholderNotification(): Notification {
|
||||||
return notification(Notifications.CHANNEL_DOWNLOADER) {
|
return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@@ -42,9 +43,9 @@ class DownloadStore(
|
|||||||
* @param downloads the list of downloads to add.
|
* @param downloads the list of downloads to add.
|
||||||
*/
|
*/
|
||||||
fun addAll(downloads: List<Download>) {
|
fun addAll(downloads: List<Download>) {
|
||||||
val editor = preferences.edit()
|
preferences.edit {
|
||||||
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
|
downloads.forEach { putString(getKey(it), serialize(it)) }
|
||||||
editor.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,14 +54,18 @@ class DownloadStore(
|
|||||||
* @param download the download to remove.
|
* @param download the download to remove.
|
||||||
*/
|
*/
|
||||||
fun remove(download: Download) {
|
fun remove(download: Download) {
|
||||||
preferences.edit().remove(getKey(download)).apply()
|
preferences.edit {
|
||||||
|
remove(getKey(download))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the downloads from the store.
|
* Removes all the downloads from the store.
|
||||||
*/
|
*/
|
||||||
fun clear() {
|
fun clear() {
|
||||||
preferences.edit().clear().apply()
|
preferences.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -137,9 +137,9 @@ class Downloader(
|
|||||||
} else {
|
} else {
|
||||||
if (notifier.paused) {
|
if (notifier.paused) {
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onPaused()
|
||||||
} else {
|
} else {
|
||||||
notifier.dismiss()
|
notifier.onComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,13 +231,9 @@ class Downloader(
|
|||||||
val wasEmpty = queue.isEmpty()
|
val wasEmpty = queue.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
val chaptersWithoutDir = async {
|
val chaptersWithoutDir = async {
|
||||||
val mangaDir = provider.findMangaDir(manga, source)
|
|
||||||
|
|
||||||
chapters
|
chapters
|
||||||
// Avoid downloading chapters with the same name.
|
|
||||||
.distinctBy { it.name }
|
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
.filter { provider.findChapterDir(it, manga, source) == null }
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.source_order }
|
.sortedByDescending { it.source_order }
|
||||||
}
|
}
|
||||||
@@ -272,6 +268,13 @@ class Downloader(
|
|||||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||||
|
|
||||||
|
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) {
|
||||||
|
download.status = Download.ERROR
|
||||||
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||||
|
return@defer Observable.just(download)
|
||||||
|
}
|
||||||
|
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
@@ -489,5 +492,8 @@ class Downloader(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TMP_DIR_SUFFIX = "_tmp"
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
|
|
||||||
|
// Arbitrary minimum required space to start a download: 50 MB
|
||||||
|
const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.github.salomonbrys.kotson.nullLong
|
||||||
|
import com.github.salomonbrys.kotson.nullString
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Scanner
|
||||||
|
|
||||||
|
class CustomMangaManager(val context: Context) {
|
||||||
|
|
||||||
|
private val editJson = File(context.getExternalFilesDir(null), "edits.json")
|
||||||
|
|
||||||
|
private var customMangaMap = mutableMapOf<Long, Manga>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
fetchCustomData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getManga(manga: Manga): Manga? = customMangaMap[manga.id]
|
||||||
|
|
||||||
|
private fun fetchCustomData() {
|
||||||
|
if (!editJson.exists() || !editJson.isFile) return
|
||||||
|
|
||||||
|
val json = try {
|
||||||
|
Gson().fromJson(
|
||||||
|
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
val mangasJson = json.get("mangas").asJsonArray ?: return
|
||||||
|
customMangaMap = mangasJson.mapNotNull { element ->
|
||||||
|
val mangaObject = element.asJsonObject ?: return@mapNotNull null
|
||||||
|
val id = mangaObject["id"]?.nullLong ?: return@mapNotNull null
|
||||||
|
val manga = MangaImpl().apply {
|
||||||
|
this.id = id
|
||||||
|
title = mangaObject["title"]?.nullString ?: ""
|
||||||
|
author = mangaObject["author"]?.nullString
|
||||||
|
artist = mangaObject["artist"]?.nullString
|
||||||
|
description = mangaObject["description"]?.nullString
|
||||||
|
genre = mangaObject["genre"]?.asJsonArray?.mapNotNull { it.nullString }
|
||||||
|
?.joinToString(", ")
|
||||||
|
}
|
||||||
|
id to manga
|
||||||
|
}.toMap().toMutableMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMangaInfo(manga: MangaJson) {
|
||||||
|
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) {
|
||||||
|
customMangaMap.remove(manga.id)
|
||||||
|
} else {
|
||||||
|
customMangaMap[manga.id] = MangaImpl().apply {
|
||||||
|
id = manga.id
|
||||||
|
title = manga.title ?: ""
|
||||||
|
author = manga.author
|
||||||
|
artist = manga.artist
|
||||||
|
description = manga.description
|
||||||
|
genre = manga.genre?.joinToString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveCustomInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCustomInfo() {
|
||||||
|
val jsonElements = customMangaMap.values.map { it.toJson() }
|
||||||
|
if (jsonElements.isNotEmpty()) {
|
||||||
|
val gson = GsonBuilder().create()
|
||||||
|
val root = JsonObject()
|
||||||
|
val mangaEntries = gson.toJsonTree(jsonElements)
|
||||||
|
|
||||||
|
root["mangas"] = mangaEntries
|
||||||
|
editJson.delete()
|
||||||
|
editJson.writeText(gson.toJson(root))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.toJson(): MangaJson {
|
||||||
|
return MangaJson(
|
||||||
|
id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MangaJson(
|
||||||
|
val id: Long,
|
||||||
|
val title: String? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
val artist: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val genre: Array<String>? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as MangaJson
|
||||||
|
if (id != other.id) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return id.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,12 @@ object Notifications {
|
|||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||||
const val ID_DOWNLOAD_CHAPTER = -201
|
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
||||||
|
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
|
||||||
|
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
||||||
|
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
||||||
|
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,7 +54,7 @@ object Notifications {
|
|||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the backup/restore system.
|
* Notification channel and ids used by the backup/restore system.
|
||||||
*/
|
*/
|
||||||
private const val GROUP_BACK_RESTORE = "group_backup_restore"
|
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
||||||
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
||||||
const val ID_BACKUP_PROGRESS = -501
|
const val ID_BACKUP_PROGRESS = -501
|
||||||
const val ID_RESTORE_PROGRESS = -503
|
const val ID_RESTORE_PROGRESS = -503
|
||||||
@@ -59,6 +63,7 @@ object Notifications {
|
|||||||
const val ID_RESTORE_COMPLETE = -504
|
const val ID_RESTORE_COMPLETE = -504
|
||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel"
|
"backup_restore_complete_channel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,10 +75,12 @@ object Notifications {
|
|||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
|
listOf(
|
||||||
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
|
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
||||||
|
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
||||||
|
).forEach(context.notificationManager::createNotificationChannelGroup)
|
||||||
|
|
||||||
val channels = listOf(
|
listOf(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
@@ -85,9 +92,24 @@ object Notifications {
|
|||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
@@ -99,26 +121,23 @@ object Notifications {
|
|||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_backup_restore_progress),
|
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACK_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_backup_restore_complete),
|
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACK_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
setSound(null, null)
|
setSound(null, null)
|
||||||
}
|
}
|
||||||
)
|
).forEach(context.notificationManager::createNotificationChannel)
|
||||||
context.notificationManager.createNotificationChannels(channels)
|
|
||||||
|
|
||||||
// Delete old notification channels
|
// Delete old notification channels
|
||||||
deprecatedChannels.forEach {
|
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
||||||
context.notificationManager.deleteNotificationChannel(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val readWithTapping = "reader_tap"
|
const val readWithTapping = "reader_tap"
|
||||||
|
|
||||||
|
const val readWithTappingInverted = "reader_tapping_inverted"
|
||||||
|
|
||||||
const val readWithLongTap = "reader_long_tap"
|
const val readWithLongTap = "reader_long_tap"
|
||||||
|
|
||||||
const val readWithVolumeKeys = "reader_volume_keys"
|
const val readWithVolumeKeys = "reader_volume_keys"
|
||||||
@@ -67,6 +69,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||||
|
|
||||||
|
const val jumpToChapters = "jump_to_chapters"
|
||||||
|
|
||||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
@@ -243,8 +247,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val eh_is_hentai_enabled = "eh_is_hentai_enabled"
|
const val eh_is_hentai_enabled = "eh_is_hentai_enabled"
|
||||||
|
|
||||||
const val eh_use_new_manga_interface = "eh_use_new_manga_interface"
|
|
||||||
|
|
||||||
const val eh_use_auto_webtoon = "eh_use_auto_webtoon"
|
const val eh_use_auto_webtoon = "eh_use_auto_webtoon"
|
||||||
|
|
||||||
const val eh_watched_list_default_state = "eh_watched_list_default_state"
|
const val eh_watched_list_default_state = "eh_watched_list_default_state"
|
||||||
@@ -268,4 +270,10 @@ object PreferenceKeys {
|
|||||||
const val sources_tab_source_categories = "sources_tab_source_categories"
|
const val sources_tab_source_categories = "sources_tab_source_categories"
|
||||||
|
|
||||||
const val sourcesSort = "sources_sort"
|
const val sourcesSort = "sources_sort"
|
||||||
|
|
||||||
|
const val recommendsInOverflow = "recommends_in_overflow"
|
||||||
|
|
||||||
|
const val hitomiAlwaysWebp = "hitomi_always_webp"
|
||||||
|
|
||||||
|
const val enhancedEHentaiView = "enhanced_e_hentai_view"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,11 @@ object PreferenceValues {
|
|||||||
COMFORTABLE_GRID,
|
COMFORTABLE_GRID,
|
||||||
LIST,
|
LIST,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class TappingInvertMode {
|
||||||
|
NONE,
|
||||||
|
HORIZONTAL,
|
||||||
|
VERTICAL,
|
||||||
|
BOTH
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
@@ -27,27 +27,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
|||||||
.onEach { block(it) }
|
.onEach { block(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
||||||
|
set(get() + item)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||||
|
set(get() - item)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val flowPrefs = FlowSharedPreferences(prefs)
|
val flowPrefs = FlowSharedPreferences(prefs)
|
||||||
|
|
||||||
private val defaultDownloadsDir = Uri.fromFile(
|
private val defaultDownloadsDir = File(
|
||||||
File(
|
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
context.getString(R.string.app_name),
|
||||||
context.getString(R.string.app_name),
|
"downloads"
|
||||||
"downloads"
|
).toUri()
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val defaultBackupDir = Uri.fromFile(
|
private val defaultBackupDir = File(
|
||||||
File(
|
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
context.getString(R.string.app_name),
|
||||||
context.getString(R.string.app_name),
|
"backup"
|
||||||
"backup"
|
).toUri()
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||||
|
|
||||||
@@ -121,6 +125,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
||||||
|
|
||||||
|
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
|
||||||
|
|
||||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||||
@@ -131,6 +137,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||||
|
|
||||||
|
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
@@ -347,8 +355,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
||||||
|
|
||||||
fun eh_useNewMangaInterface() = flowPrefs.getBoolean(Keys.eh_use_new_manga_interface, true)
|
|
||||||
|
|
||||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
||||||
|
|
||||||
fun eh_watchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false)
|
fun eh_watchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false)
|
||||||
@@ -368,4 +374,10 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
|
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
|
||||||
|
|
||||||
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
|
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
|
||||||
|
|
||||||
|
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
||||||
|
|
||||||
|
fun hitomiAlwaysWebp() = flowPrefs.getBoolean(Keys.hitomiAlwaysWebp, true)
|
||||||
|
|
||||||
|
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.get
|
import com.github.salomonbrys.kotson.get
|
||||||
import com.github.salomonbrys.kotson.jsonObject
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
@@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
return baseMangaUrl + mediaId
|
return baseMangaUrl + mediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("response_type", "token")
|
.appendQueryParameter("response_type", "token")
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.obj
|
import com.github.salomonbrys.kotson.obj
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
@@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
val url = Uri.parse(
|
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
.toUri()
|
||||||
).buildUpon()
|
.buildUpon()
|
||||||
.appendQueryParameter("max_results", "20")
|
.appendQueryParameter("max_results", "20")
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
@@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
return "$baseMangaUrl/$remoteId"
|
return "$baseMangaUrl/$remoteId"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() =
|
fun authUrl(): Uri =
|
||||||
Uri.parse(loginUrl).buildUpon()
|
loginUrl.toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
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.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
@@ -260,13 +260,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
|
|
||||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||||
|
|
||||||
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
private fun loginUrl() = baseUrl.toUri().buildUpon()
|
||||||
.appendPath("login.php")
|
.appendPath("login.php")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun searchUrl(query: String): String {
|
private fun searchUrl(query: String): String {
|
||||||
val col = "c[]"
|
val col = "c[]"
|
||||||
return Uri.parse(baseUrl).buildUpon()
|
return baseUrl.toUri().buildUpon()
|
||||||
.appendPath("manga.php")
|
.appendPath("manga.php")
|
||||||
.appendQueryParameter("q", query)
|
.appendQueryParameter("q", query)
|
||||||
.appendQueryParameter(col, "a")
|
.appendQueryParameter(col, "a")
|
||||||
@@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
private fun exportListUrl() = baseUrl.toUri().buildUpon()
|
||||||
.appendPath("panel.php")
|
.appendPath("panel.php")
|
||||||
.appendQueryParameter("go", "export")
|
.appendQueryParameter("go", "export")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
|
||||||
.appendPath(mediaId.toString())
|
.appendPath(mediaId.toString())
|
||||||
.appendPath("edit")
|
.appendPath("edit")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
|
||||||
.appendPath("add.json")
|
.appendPath("add.json")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikimori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.jsonObject
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
import com.github.salomonbrys.kotson.nullString
|
import com.github.salomonbrys.kotson.nullString
|
||||||
@@ -54,7 +54,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||||
.appendQueryParameter("order", "popularity")
|
.appendQueryParameter("order", "popularity")
|
||||||
.appendQueryParameter("search", search)
|
.appendQueryParameter("search", search)
|
||||||
.appendQueryParameter("limit", "20")
|
.appendQueryParameter("limit", "20")
|
||||||
@@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||||
.appendQueryParameter("user_id", user_id)
|
.appendQueryParameter("user_id", user_id)
|
||||||
.appendQueryParameter("target_id", track.media_id.toString())
|
.appendQueryParameter("target_id", track.media_id.toString())
|
||||||
.appendQueryParameter("target_type", "Manga")
|
.appendQueryParameter("target_type", "Manga")
|
||||||
@@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||||
.appendPath(track.media_id.toString())
|
.appendPath(track.media_id.toString())
|
||||||
.build()
|
.build()
|
||||||
val requestMangas = Request.Builder()
|
val requestMangas = Request.Builder()
|
||||||
@@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() =
|
fun authUrl() =
|
||||||
Uri.parse(loginUrl).buildUpon()
|
loginUrl.toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
|
|||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@@ -20,7 +21,6 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
import exh.EIGHTMUSES_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
import exh.HBROWSE_SOURCE_ID
|
|
||||||
import exh.HITOMI_SOURCE_ID
|
import exh.HITOMI_SOURCE_ID
|
||||||
import exh.MERGED_SOURCE_ID
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.NHENTAI_SOURCE_ID
|
import exh.NHENTAI_SOURCE_ID
|
||||||
@@ -88,7 +88,6 @@ class ExtensionManager(
|
|||||||
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
|
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
|
||||||
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
|
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
|
||||||
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
|
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
|
||||||
HBROWSE_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hbrowse_source)
|
|
||||||
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -319,8 +318,7 @@ class ExtensionManager(
|
|||||||
if (signature !in untrustedSignatures) return
|
if (signature !in untrustedSignatures) return
|
||||||
|
|
||||||
ExtensionLoader.trustedSignatures += signature
|
ExtensionLoader.trustedSignatures += signature
|
||||||
val preference = preferences.trustedSignatures()
|
preferences.trustedSignatures() += signature
|
||||||
preference.set(preference.get() + signature)
|
|
||||||
|
|
||||||
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
||||||
untrustedExtensions -= nowTrustedExtensions
|
untrustedExtensions -= nowTrustedExtensions
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@@ -63,7 +65,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
// Register the receiver after removing (and unregistering) the previous download
|
// Register the receiver after removing (and unregistering) the previous download
|
||||||
downloadReceiver.register()
|
downloadReceiver.register()
|
||||||
|
|
||||||
val downloadUri = Uri.parse(url)
|
val downloadUri = url.toUri()
|
||||||
val request = DownloadManager.Request(downloadUri)
|
val request = DownloadManager.Request(downloadUri)
|
||||||
.setTitle(extension.name)
|
.setTitle(extension.name)
|
||||||
.setMimeType(APK_MIME)
|
.setMimeType(APK_MIME)
|
||||||
@@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param pkgName The package name of the extension to uninstall
|
* @param pkgName The package name of the extension to uninstall
|
||||||
*/
|
*/
|
||||||
fun uninstallApk(pkgName: String) {
|
fun uninstallApk(pkgName: String) {
|
||||||
val packageUri = Uri.parse("package:$pkgName")
|
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import androidx.webkit.WebViewClientCompat
|
|||||||
import androidx.webkit.WebViewFeature
|
import androidx.webkit.WebViewFeature
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
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.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
@@ -42,9 +45,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
if (!WebViewUtil.supportsWebView(context)) {
|
||||||
|
launchUI {
|
||||||
|
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
return chain.proceed(originalRequest)
|
||||||
|
}
|
||||||
|
|
||||||
initWebView
|
initWebView
|
||||||
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
val response = chain.proceed(originalRequest)
|
val response = chain.proceed(originalRequest)
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
@@ -85,7 +96,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
handler.post {
|
handler.post {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
webView = webview
|
webView = webview
|
||||||
webview.settings.javaScriptEnabled = true
|
webview.setDefaultSettings()
|
||||||
|
|
||||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||||
webview.settings.userAgentString = request.header("User-Agent")
|
webview.settings.userAgentString = request.header("User-Agent")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
@@ -28,13 +29,15 @@ import timber.log.Timber
|
|||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
class LocalSource(private val context: Context) : CatalogueSource {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
||||||
|
|
||||||
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 POPULAR_FILTERS = FilterList(OrderBy())
|
private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
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)
|
||||||
const val ID = 0L
|
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||||
val dir = getBaseDirectories(context).firstOrNull()
|
val dir = getBaseDirectories(context).firstOrNull()
|
||||||
@@ -73,9 +76,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirs = getBaseDirectories(context)
|
||||||
|
|
||||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }
|
var mangaDirs = baseDirs
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { it.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
.filter { it.isDirectory }
|
||||||
|
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
|
||||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||||
@@ -132,13 +138,55 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Observable.just(MangasPage(mangas, false))
|
|
||||||
|
return Observable.just(MangasPage(mangas.toList(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun updateMangaInfo(manga: SManga) {
|
||||||
|
val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find {
|
||||||
|
it.exists()
|
||||||
|
} ?: return
|
||||||
|
val gson = GsonBuilder().setPrettyPrinting().create()
|
||||||
|
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
||||||
|
val file = File(directory, existingFileName ?: "info.json")
|
||||||
|
file.writeText(gson.toJson(manga.toJson()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SManga.toJson(): MangaJson {
|
||||||
|
return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MangaJson(
|
||||||
|
val title: String,
|
||||||
|
val author: String?,
|
||||||
|
val artist: String?,
|
||||||
|
val description: String?,
|
||||||
|
val genre: Array<String>?
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as MangaJson
|
||||||
|
|
||||||
|
if (title != other.title) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return title.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
getBaseDirectories(context)
|
getBaseDirectories(context)
|
||||||
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.firstOrNull { it.extension == "json" }
|
.firstOrNull { it.extension == "json" }
|
||||||
@@ -154,6 +202,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
?: manga.genre
|
?: manga.genre
|
||||||
manga.status = json["status"]?.asInt ?: manga.status
|
manga.status = json["status"]?.asInt ?: manga.status
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.just(manga)
|
return Observable.just(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,8 +253,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
var chapterNameIndex = 0
|
var chapterNameIndex = 0
|
||||||
var mangaTitleIndex = 0
|
var mangaTitleIndex = 0
|
||||||
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||||
val chapterChar = chapterName.get(chapterNameIndex)
|
val chapterChar = chapterName[chapterNameIndex]
|
||||||
val mangaChar = mangaTitle.get(mangaTitleIndex)
|
val mangaChar = mangaTitle[mangaTitleIndex]
|
||||||
if (!chapterChar.equals(mangaChar, true)) {
|
if (!chapterChar.equals(mangaChar, true)) {
|
||||||
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||||
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||||
@@ -235,7 +284,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.toLowerCase() in setOf("zip", "rar", "cbr", "cbz", "epub")
|
return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
@@ -269,8 +318,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
return when (val format = getFormat(chapter)) {
|
return when (val format = getFormat(chapter)) {
|
||||||
is Format.Directory -> {
|
is Format.Directory -> {
|
||||||
val entry = format.file.listFiles()
|
val entry = format.file.listFiles()
|
||||||
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
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
|
||||||
@@ -31,7 +32,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
// Recreate sources when they change
|
// Recreate sources when they change
|
||||||
prefs.enableExhentai().asFlow().onEach {
|
prefs.enableExhentai().asImmediateFlow {
|
||||||
createEHSources().forEach { registerSource(it) }
|
createEHSources().forEach { registerSource(it) }
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
@@ -72,6 +72,10 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
||||||
|
|
||||||
|
fun getVisibleOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>().filter {
|
||||||
|
it.id !in BlacklistedSources.HIDDEN_SOURCES
|
||||||
|
}
|
||||||
|
|
||||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -98,7 +102,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
||||||
val enhancedSource = EnhancedHttpSource(
|
val enhancedSource = EnhancedHttpSource(
|
||||||
source,
|
source,
|
||||||
delegate.newSourceClass.constructors.find { it.parameters.size == 1 }!!.call(source)
|
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||||
)
|
)
|
||||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.originalSourceQualifiedClassName }
|
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.originalSourceQualifiedClassName }
|
||||||
currentDelegatedSources.plusAssign(map)
|
currentDelegatedSources.plusAssign(map)
|
||||||
@@ -132,12 +136,11 @@ open class SourceManager(private val context: Context) {
|
|||||||
if (prefs.enableExhentai().get()) {
|
if (prefs.enableExhentai().get()) {
|
||||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
||||||
}
|
}
|
||||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
|
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en, context)
|
||||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
|
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it, context)
|
||||||
exSrcs += NHentai(context)
|
exSrcs += NHentai(context)
|
||||||
exSrcs += Hitomi()
|
exSrcs += Hitomi(context)
|
||||||
exSrcs += EightMuses()
|
exSrcs += EightMuses(context)
|
||||||
exSrcs += HBrowse()
|
|
||||||
return exSrcs
|
return exSrcs
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -196,7 +199,13 @@ open class SourceManager(private val context: Context) {
|
|||||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||||
MangaDex::class,
|
MangaDex::class,
|
||||||
true
|
true
|
||||||
)*/
|
)*/,
|
||||||
|
DelegatedSource(
|
||||||
|
"HBrowse",
|
||||||
|
1401584337232758222,
|
||||||
|
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
||||||
|
HBrowse::class
|
||||||
|
)
|
||||||
).associateBy { it.originalSourceQualifiedClassName }
|
).associateBy { it.originalSourceQualifiedClassName }
|
||||||
|
|
||||||
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
|
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ sealed class Filter<T>(val name: String, var state: T) {
|
|||||||
data class Selection(val index: Int, val ascending: Boolean)
|
data class Selection(val index: Int, val ascending: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 <--
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
|
|
||||||
|
/* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
|
||||||
|
// SY <--
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SManga : Serializable {
|
interface SManga : Serializable {
|
||||||
@@ -22,27 +23,40 @@ interface SManga : Serializable {
|
|||||||
|
|
||||||
var initialized: Boolean
|
var initialized: Boolean
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
val originalTitle: String
|
||||||
|
get() = (this as? MangaImpl)?.ogTitle ?: title
|
||||||
|
val originalAuthor: String?
|
||||||
|
get() = (this as? MangaImpl)?.ogAuthor ?: author
|
||||||
|
val originalArtist: String?
|
||||||
|
get() = (this as? MangaImpl)?.ogArtist ?: artist
|
||||||
|
val originalDescription: String?
|
||||||
|
get() = (this as? MangaImpl)?.ogDesc ?: description
|
||||||
|
val originalGenre: String?
|
||||||
|
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun copyFrom(other: SManga) {
|
fun copyFrom(other: SManga) {
|
||||||
// EXH -->
|
// EXH -->
|
||||||
if (other.title.isNotBlank()) {
|
if (other.title.isNotBlank()) {
|
||||||
title = other.title
|
title = other.originalTitle
|
||||||
}
|
}
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
if (other.author != null) {
|
if (other.author != null) {
|
||||||
author = other.author
|
author = /* SY --> */ other.originalAuthor /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (other.artist != null) {
|
if (other.artist != null) {
|
||||||
artist = other.artist
|
artist = /* SY --> */ other.originalArtist /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (other.description != null) {
|
if (other.description != null) {
|
||||||
description = other.description
|
description = /* SY --> */ other.originalDescription /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (other.genre != null) {
|
if (other.genre != null) {
|
||||||
genre = other.genre
|
genre = /* SY --> */ other.originalGenre /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (other.thumbnail_url != null) {
|
if (other.thumbnail_url != null) {
|
||||||
@@ -61,9 +75,6 @@ interface SManga : Serializable {
|
|||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val LICENSED = 3
|
const val LICENSED = 3
|
||||||
// SY -->
|
|
||||||
const val RECOMMENDS = 69 // nice
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
fun create(): SManga {
|
fun create(): SManga {
|
||||||
return SMangaImpl()
|
return SMangaImpl()
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.Source
|
||||||
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.ui.manga.MangaController
|
||||||
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.source.EnhancedHttpSource
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
@@ -102,6 +106,24 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDescriptionAdapter(controller: MangaController): RecyclerView.Adapter<*>?
|
||||||
|
|
||||||
val SManga.id get() = (this as? Manga)?.id
|
val SManga.id get() = (this as? Manga)?.id
|
||||||
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Source.isLewdSource() = (this is LewdSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>))
|
||||||
|
|
||||||
|
fun Source.getLewdSource(): LewdSource<*, *>? {
|
||||||
|
return if (!this.isLewdSource()) {
|
||||||
|
null
|
||||||
|
} else if (this is LewdSource<*, *>) {
|
||||||
|
this
|
||||||
|
} else if (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>) {
|
||||||
|
this.enhancedSource
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.eh.EHTags
|
import exh.eh.EHTags
|
||||||
@@ -36,15 +37,19 @@ import exh.metadata.metadata.EHentaiSearchMetadata
|
|||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
||||||
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.metadata.nullIfBlank
|
|
||||||
import exh.metadata.parseHumanReadableByteCount
|
import exh.metadata.parseHumanReadableByteCount
|
||||||
import exh.ui.login.LoginController
|
import exh.ui.login.LoginController
|
||||||
|
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.ignore
|
import exh.util.ignore
|
||||||
|
import exh.util.nullIfBlank
|
||||||
|
import exh.util.trimOrNull
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
@@ -91,9 +96,9 @@ class EHentai(
|
|||||||
/**
|
/**
|
||||||
* Gallery list entry
|
* Gallery list entry
|
||||||
*/
|
*/
|
||||||
data class ParsedManga(val fav: Int, val manga: Manga)
|
data class ParsedManga(val fav: Int, val manga: Manga, val metadata: EHentaiSearchMetadata)
|
||||||
|
|
||||||
fun extendedGenericMangaParse(doc: Document) = with(doc) {
|
private fun extendedGenericMangaParse(doc: Document) = with(doc) {
|
||||||
// Parse mangas (supports compact + extended layout)
|
// Parse mangas (supports compact + extended layout)
|
||||||
val parsedMangas = select(".itg > tbody > tr").filter {
|
val parsedMangas = select(".itg > tbody > tr").filter {
|
||||||
// Do not parse header and ads
|
// Do not parse header and ads
|
||||||
@@ -102,8 +107,11 @@ class EHentai(
|
|||||||
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||||
val column2 = it.selectFirst(".gl3e, .gl2c")
|
val column2 = it.selectFirst(".gl3e, .gl2c")
|
||||||
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||||
|
val infoElement = it.selectFirst(".gl3e")
|
||||||
|
|
||||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||||
|
val infoElements = infoElement?.select("div")
|
||||||
|
val parsedTags = mutableListOf<RaisedTag>()
|
||||||
|
|
||||||
ParsedManga(
|
ParsedManga(
|
||||||
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
||||||
@@ -116,7 +124,78 @@ class EHentai(
|
|||||||
// Get image
|
// Get image
|
||||||
thumbnail_url = thumbnailElement.attr("src")
|
thumbnail_url = thumbnailElement.attr("src")
|
||||||
|
|
||||||
// TODO Parse genre + uploader + tags
|
if (infoElements != null) {
|
||||||
|
linkElement.select("div div")?.getOrNull(1)?.select("tr")?.forEach { row ->
|
||||||
|
val namespace = row.select(".tc").text().removeSuffix(":")
|
||||||
|
parsedTags.addAll(
|
||||||
|
row.select("div").map { element ->
|
||||||
|
RaisedTag(
|
||||||
|
namespace,
|
||||||
|
element.text().trim(),
|
||||||
|
when {
|
||||||
|
element.hasClass("gtl") -> TAG_TYPE_LIGHT
|
||||||
|
element.hasClass("gtw") -> TAG_TYPE_WEAK
|
||||||
|
else -> TAG_TYPE_NORMAL
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val tagElement = it.selectFirst(".gl3c > a")
|
||||||
|
val tagElements = tagElement.select("div")
|
||||||
|
tagElements.forEach { element ->
|
||||||
|
if (element.className() == "gt") {
|
||||||
|
val namespace = element.attr("title").substringBefore(":").trimOrNull() ?: "misc"
|
||||||
|
parsedTags += RaisedTag(
|
||||||
|
namespace,
|
||||||
|
element.attr("title").substringAfter(":").trim(),
|
||||||
|
TAG_TYPE_NORMAL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genre = parsedTags.toGenreString()
|
||||||
|
},
|
||||||
|
metadata = EHentaiSearchMetadata().apply {
|
||||||
|
tags += parsedTags
|
||||||
|
|
||||||
|
if (infoElements != null) {
|
||||||
|
getGenre(infoElements.getOrNull(1))?.let { genre = it }
|
||||||
|
|
||||||
|
getDateTag(infoElements.getOrNull(2))?.let { datePosted = it }
|
||||||
|
|
||||||
|
getRating(infoElements.getOrNull(3))?.let { averageRating = it }
|
||||||
|
|
||||||
|
getUploader(infoElements.getOrNull(4))?.let { uploader = it }
|
||||||
|
|
||||||
|
getPageCount(infoElements.getOrNull(5))?.let { length = it }
|
||||||
|
} else {
|
||||||
|
val parsedGenre = it.selectFirst(".gl1c div")
|
||||||
|
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
|
||||||
|
|
||||||
|
val info = it.selectFirst(".gl2c")
|
||||||
|
val extraInfo = it.selectFirst(".gl4c")
|
||||||
|
|
||||||
|
val infoList = info.select("div div")
|
||||||
|
|
||||||
|
getDateTag(infoList.getOrNull(8))?.let { datePosted = it }
|
||||||
|
|
||||||
|
getRating(infoList.getOrNull(9))?.let { averageRating = it }
|
||||||
|
|
||||||
|
val extraInfoList = extraInfo.select("div")
|
||||||
|
|
||||||
|
if (extraInfoList.getOrNull(2) == null) {
|
||||||
|
getUploader(extraInfoList.getOrNull(0))?.let { uploader = it }
|
||||||
|
|
||||||
|
getPageCount(extraInfoList.getOrNull(1))?.let { length = it }
|
||||||
|
} else {
|
||||||
|
getUploader(extraInfoList.getOrNull(1))?.let { uploader = it }
|
||||||
|
|
||||||
|
getPageCount(extraInfoList.getOrNull(2))?.let { length = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -136,11 +215,54 @@ class EHentai(
|
|||||||
Pair(parsedMangas, hasNextPage)
|
Pair(parsedMangas, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
|
||||||
|
return element?.attr("onclick")
|
||||||
|
?.nullIfBlank()
|
||||||
|
?.substringAfterLast('/')
|
||||||
|
?.removeSuffix("'")
|
||||||
|
?.trim()
|
||||||
|
?.substringAfterLast('/')
|
||||||
|
?.removeSuffix("'") ?: genreString
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDateTag(element: Element?): Long? {
|
||||||
|
val text = element?.text()?.nullIfBlank()
|
||||||
|
return if (text != null) {
|
||||||
|
val date = EX_DATE_FORMAT.parse(text)
|
||||||
|
date?.time
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRating(element: Element?): Double? {
|
||||||
|
val ratingStyle = element?.attr("style")?.nullIfBlank()
|
||||||
|
return if (ratingStyle != null) {
|
||||||
|
val matches = RATING_REGEX.findAll(ratingStyle).mapNotNull { it.groupValues.getOrNull(1)?.toIntOrNull() }.toList()
|
||||||
|
if (matches.size == 2) {
|
||||||
|
var rate = 5 - matches[0] / 16
|
||||||
|
if (matches[1] == 21) {
|
||||||
|
rate--
|
||||||
|
rate + 0.5
|
||||||
|
} else rate.toDouble()
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUploader(element: Element?): String? {
|
||||||
|
return element?.select("a")?.text()?.trimOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPageCount(element: Element?): Int? {
|
||||||
|
val pageCount = element?.text()?.trimOrNull()
|
||||||
|
return if (pageCount != null) {
|
||||||
|
PAGE_COUNT_REGEX.find(pageCount)?.value?.toIntOrNull()
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a list of galleries
|
* Parse a list of galleries
|
||||||
*/
|
*/
|
||||||
fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let {
|
fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let {
|
||||||
MangasPage(it.first.map { it.manga }, it.second)
|
MetadataMangasPage(it.first.map { it.manga }, it.second, it.first.map { it.metadata })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
|
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
|
||||||
@@ -271,7 +393,7 @@ class EHentai(
|
|||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||||
client.newCall(it).asObservableSuccess()
|
client.newCall(it).asObservableSuccess()
|
||||||
}.map { response ->
|
}.map { response ->
|
||||||
@@ -477,6 +599,8 @@ class EHentai(
|
|||||||
element.text().trim(),
|
element.text().trim(),
|
||||||
if (element.hasClass("gtl")) {
|
if (element.hasClass("gtl")) {
|
||||||
TAG_TYPE_LIGHT
|
TAG_TYPE_LIGHT
|
||||||
|
} else if (element.hasClass("gtw")) {
|
||||||
|
TAG_TYPE_WEAK
|
||||||
} else {
|
} else {
|
||||||
TAG_TYPE_NORMAL
|
TAG_TYPE_NORMAL
|
||||||
}
|
}
|
||||||
@@ -550,7 +674,7 @@ class EHentai(
|
|||||||
page++
|
page++
|
||||||
} while (parsed.second)
|
} while (parsed.second)
|
||||||
|
|
||||||
return Pair(result as List<ParsedManga>, favNames!!)
|
return Pair(result.toList(), favNames!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun spPref() = if (exh) {
|
fun spPref() = if (exh) {
|
||||||
@@ -832,10 +956,16 @@ class EHentai(
|
|||||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): EHentaiDescriptionAdapter {
|
||||||
|
return EHentaiDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||||
private const val TR_SUFFIX = "TR"
|
private const val TR_SUFFIX = "TR"
|
||||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||||
|
private val PAGE_COUNT_REGEX = "[0-9]*".toRegex()
|
||||||
|
private val RATING_REGEX = "([0-9]*)px".toRegex()
|
||||||
|
|
||||||
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
|
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
|
||||||
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
@@ -17,6 +18,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.HITOMI_SOURCE_ID
|
import exh.HITOMI_SOURCE_ID
|
||||||
import exh.hitomi.HitomiNozomi
|
import exh.hitomi.HitomiNozomi
|
||||||
@@ -26,6 +28,7 @@ import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
|
|||||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -41,7 +44,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
/**
|
/**
|
||||||
* Man, I hate this source :(
|
* Man, I hate this source :(
|
||||||
*/
|
*/
|
||||||
class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource {
|
class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource {
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
override val id = HITOMI_SOURCE_ID
|
override val id = HITOMI_SOURCE_ID
|
||||||
@@ -185,7 +188,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
return urlImportFetchSearchManga(query) {
|
return urlImportFetchSearchManga(context, query) {
|
||||||
val splitQuery = query.split(" ")
|
val splitQuery = query.split(" ")
|
||||||
|
|
||||||
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
|
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
|
||||||
@@ -303,9 +306,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
val titleElement = doc.selectFirst("h1")
|
val titleElement = doc.selectFirst("h1")
|
||||||
title = titleElement.text()
|
title = titleElement.text()
|
||||||
thumbnail_url = "https:" + if (prefs.eh_hl_useHighQualityThumbs().get()) {
|
thumbnail_url = "https:" + if (prefs.eh_hl_useHighQualityThumbs().get()) {
|
||||||
doc.selectFirst("img").attr("data-srcset").substringBefore(' ')
|
doc.selectFirst("img").attr("srcset").substringBefore(' ')
|
||||||
} else {
|
} else {
|
||||||
doc.selectFirst("img").attr("data-src")
|
doc.selectFirst("img").attr("src")
|
||||||
}
|
}
|
||||||
url = titleElement.child(0).attr("href")
|
url = titleElement.child(0).attr("href")
|
||||||
|
|
||||||
@@ -374,8 +377,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
val json = JsonParser.parseString(str.removePrefix("var galleryinfo = "))
|
val json = JsonParser.parseString(str.removePrefix("var galleryinfo = "))
|
||||||
return json["files"].array.mapIndexed { index, jsonElement ->
|
return json["files"].array.mapIndexed { index, jsonElement ->
|
||||||
val hash = jsonElement["hash"].string
|
val hash = jsonElement["hash"].string
|
||||||
val ext = if (jsonElement["haswebp"].string == "0") jsonElement["name"].string.split('.').last() else "webp"
|
val ext = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) jsonElement["name"].string.split('.').last() else "webp"
|
||||||
val path = if (jsonElement["haswebp"].string == "0") "images" else "webp"
|
val path = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) "images" else "webp"
|
||||||
val hashPath1 = hash.takeLast(1)
|
val hashPath1 = hash.takeLast(1)
|
||||||
val hashPath2 = hash.takeLast(3).take(2)
|
val hashPath2 = hash.takeLast(3).take(2)
|
||||||
Page(
|
Page(
|
||||||
@@ -421,6 +424,10 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
|
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): HitomiDescriptionAdapter {
|
||||||
|
return HitomiDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
||||||
private val PAGE_SIZE = 25
|
private val PAGE_SIZE = 25
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
@@ -11,7 +12,7 @@ import exh.source.DelegatedHttpSource
|
|||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class MangaDex(delegate: HttpSource) :
|
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
ConfigurableSource,
|
ConfigurableSource,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
@@ -19,7 +20,7 @@ class MangaDex(delegate: HttpSource) :
|
|||||||
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.NHENTAI_SOURCE_ID
|
import exh.NHENTAI_SOURCE_ID
|
||||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||||
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
@@ -37,7 +40,7 @@ import rx.Observable
|
|||||||
* NHentai source
|
* NHentai source
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
|
class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
|
||||||
override val metaClass = NHentaiSearchMetadata::class
|
override val metaClass = NHentaiSearchMetadata::class
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
@@ -57,7 +60,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
"$baseUrl/g/$trimmedIdQuery/"
|
"$baseUrl/g/$trimmedIdQuery/"
|
||||||
} else query
|
} else query
|
||||||
|
|
||||||
return urlImportFetchSearchManga(newQuery) {
|
return urlImportFetchSearchManga(context, newQuery) {
|
||||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||||
client.newCall(it).asObservableSuccess()
|
client.newCall(it).asObservableSuccess()
|
||||||
}.map { response ->
|
}.map { response ->
|
||||||
@@ -195,7 +198,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
tags.clear()
|
tags.clear()
|
||||||
}?.forEach {
|
}?.forEach {
|
||||||
if (it.first != null && it.second != null) {
|
if (it.first != null && it.second != null) {
|
||||||
tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT))
|
tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") TAG_TYPE_VIRTUAL else TAG_TYPE_DEFAULT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,6 +369,10 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
return "$baseUrl/g/${uri.pathSegments[1]}/"
|
return "$baseUrl/g/${uri.pathSegments[1]}/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): NHentaiDescriptionAdapter {
|
||||||
|
return NHentaiDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
||||||
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
|
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
@@ -12,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import exh.metadata.metadata.PervEdenLang
|
import exh.metadata.metadata.PervEdenLang
|
||||||
@@ -19,6 +21,7 @@ import exh.metadata.metadata.PervEdenSearchMetadata
|
|||||||
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
|
||||||
import exh.util.UriFilter
|
import exh.util.UriFilter
|
||||||
import exh.util.UriGroup
|
import exh.util.UriGroup
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
@@ -33,7 +36,7 @@ import org.jsoup.nodes.TextNode
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
// TODO Transform into delegated source
|
// TODO Transform into delegated source
|
||||||
class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Context) :
|
||||||
ParsedHttpSource(),
|
ParsedHttpSource(),
|
||||||
LewdSource<PervEdenSearchMetadata, Document>,
|
LewdSource<PervEdenSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
@@ -64,7 +67,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
|||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +360,10 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
|||||||
return newUri.toString()
|
return newUri.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
|
||||||
|
return PervEdenDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
|
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
|
||||||
timeZone = TimeZone.getTimeZone("GMT")
|
timeZone = TimeZone.getTimeZone("GMT")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.kizitonwose.time.hours
|
import com.kizitonwose.time.hours
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
@@ -13,10 +14,12 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
import exh.EIGHTMUSES_SOURCE_ID
|
||||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
||||||
import exh.util.CachedField
|
import exh.util.CachedField
|
||||||
import exh.util.NakedTrie
|
import exh.util.NakedTrie
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
@@ -41,7 +44,7 @@ import rx.schedulers.Schedulers
|
|||||||
|
|
||||||
typealias SiteMap = NakedTrie<Unit>
|
typealias SiteMap = NakedTrie<Unit>
|
||||||
|
|
||||||
class EightMuses :
|
class EightMuses(val context: Context) :
|
||||||
HttpSource(),
|
HttpSource(),
|
||||||
LewdSource<EightMusesSearchMetadata, Document>,
|
LewdSource<EightMusesSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
@@ -177,7 +180,7 @@ class EightMuses :
|
|||||||
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
|
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
return urlImportFetchSearchManga(query) {
|
return urlImportFetchSearchManga(context, query) {
|
||||||
fetchListing(searchMangaRequest(page, query, filters), false)
|
fetchListing(searchMangaRequest(page, query, filters), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +277,7 @@ class EightMuses :
|
|||||||
// Request
|
// Request
|
||||||
val req = eightMusesGet(baseUrl + url)
|
val req = eightMusesGet(baseUrl + url)
|
||||||
|
|
||||||
return client.newCall(req).asObservableSuccess().toSingle().await(Schedulers.io()).use { response ->
|
return client.newCall(req).asObservableSuccess().toSingle().toBlocking().value().use { response ->
|
||||||
val contents = parseSelf(response.asJsoup())
|
val contents = parseSelf(response.asJsoup())
|
||||||
|
|
||||||
val out = mutableListOf<SChapter>()
|
val out = mutableListOf<SChapter>()
|
||||||
@@ -396,4 +399,8 @@ class EightMuses :
|
|||||||
}
|
}
|
||||||
return "/comics/album/${path.joinToString("/")}"
|
return "/comics/album/${path.joinToString("/")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): EightMusesDescriptionAdapter {
|
||||||
|
return EightMusesDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,328 +1,55 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.array
|
|
||||||
import com.github.salomonbrys.kotson.string
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.HBROWSE_SOURCE_ID
|
|
||||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.search.Namespace
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.search.SearchEngine
|
import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
|
||||||
import exh.search.Text
|
|
||||||
import exh.util.await
|
|
||||||
import exh.util.dropBlank
|
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
|
||||||
import info.debatty.java.stringsimilarity.Levenshtein
|
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.rx2.asSingle
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
|
|
||||||
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
|
|
||||||
/**
|
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
|
||||||
*/
|
|
||||||
override val lang: String = "en"
|
|
||||||
/**
|
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
|
||||||
*/
|
|
||||||
override val baseUrl = HBrowseSearchMetadata.BASE_URL
|
|
||||||
|
|
||||||
override val name: String = "HBrowse"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
|
class HBrowse(delegate: HttpSource, val context: Context) :
|
||||||
|
DelegatedHttpSource(delegate),
|
||||||
|
LewdSource<HBrowseSearchMetadata, Document>,
|
||||||
|
UrlImportableSource {
|
||||||
override val metaClass = HBrowseSearchMetadata::class
|
override val metaClass = HBrowseSearchMetadata::class
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
override val id: Long = HBROWSE_SOURCE_ID
|
// Support direct URL importing
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
|
urlImportFetchSearchManga(context, query) {
|
||||||
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder()
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
.add("Cookie", BASE_COOKIES)
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
private val clientWithoutCookies = client.newBuilder()
|
.flatMap {
|
||||||
.cookieJar(CookieJar.NO_COOKIES)
|
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||||
.build()
|
|
||||||
|
|
||||||
private val nonRedirectingClientWithoutCookies = clientWithoutCookies.newBuilder()
|
|
||||||
.followRedirects(false)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val searchEngine = SearchEngine()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the popular manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/browse/title/rank/DESC/$page", headers)
|
|
||||||
|
|
||||||
private fun parseListing(response: Response): MangasPage {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val main = doc.selectFirst("#main")
|
|
||||||
val items = main.select(".thumbTable > tbody")
|
|
||||||
val manga = items.map { mangaEle ->
|
|
||||||
SManga.create().apply {
|
|
||||||
val thumbElement = mangaEle.selectFirst(".thumbImg")
|
|
||||||
url = "/" + thumbElement.parent().attr("href").split("/").dropBlank().first()
|
|
||||||
title = thumbElement.parent().attr("title").substringAfter('\'').substringBeforeLast('\'')
|
|
||||||
thumbnail_url = baseUrl + thumbElement.attr("src")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
|
|
||||||
return MangasPage(
|
|
||||||
manga,
|
|
||||||
hasNextPage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return urlImportFetchSearchManga(query) {
|
|
||||||
fetchSearchMangaInternal(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun popularMangaParse(response: Response) = parseListing(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the search manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Should not be called!")
|
|
||||||
|
|
||||||
private fun fetchSearchMangaInternal(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return RxJavaInterop.toV1Single(
|
|
||||||
GlobalScope.async(Dispatchers.IO) {
|
|
||||||
val modeFilter = filters.filterIsInstance<ModeFilter>().firstOrNull()
|
|
||||||
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()
|
|
||||||
|
|
||||||
var base: String? = null
|
|
||||||
var isSortFilter = false
|
|
||||||
// <NS, VALUE, EXCLUDED>
|
|
||||||
var tagQuery: List<Triple<String, String, Boolean>>? = null
|
|
||||||
|
|
||||||
if (sortFilter != null) {
|
|
||||||
sortFilter.state?.let { state ->
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
throw IllegalArgumentException("Cannot use sorting while text/tag search is active!")
|
|
||||||
}
|
|
||||||
|
|
||||||
isSortFilter = true
|
|
||||||
base = "/browse/title/${SortFilter.SORT_OPTIONS[state.index].first}/${if (state.ascending) "ASC" else "DESC"}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (base == null) {
|
|
||||||
base = if (modeFilter != null && modeFilter.state == 1) {
|
|
||||||
tagQuery = searchEngine.parseQuery(query, false).map {
|
|
||||||
when (it) {
|
|
||||||
is Text -> {
|
|
||||||
var minDist = Int.MAX_VALUE.toDouble()
|
|
||||||
// ns, value
|
|
||||||
var minContent: Pair<String, String> = "" to ""
|
|
||||||
for (ns in ALL_TAGS) {
|
|
||||||
val (v, d) = ns.value.nearest(it.rawTextOnly(), minDist)
|
|
||||||
if (d < minDist) {
|
|
||||||
minDist = d
|
|
||||||
minContent = ns.key to v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
minContent
|
|
||||||
}
|
|
||||||
is Namespace -> {
|
|
||||||
// Map ns aliases
|
|
||||||
val mappedNs = NS_MAPPINGS[it.namespace] ?: it.namespace
|
|
||||||
|
|
||||||
var key = mappedNs
|
|
||||||
if (!ALL_TAGS.containsKey(key)) key = ALL_TAGS.keys.sorted().nearest(mappedNs).first
|
|
||||||
|
|
||||||
// Find nearest NS
|
|
||||||
val nsContents = ALL_TAGS[key]
|
|
||||||
|
|
||||||
key to nsContents!!.nearest(it.tag?.rawTextOnly() ?: "").first
|
|
||||||
}
|
|
||||||
else -> error("Unknown type!")
|
|
||||||
}.let { p ->
|
|
||||||
Triple(p.first, p.second, it.excluded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"/result"
|
|
||||||
} else {
|
|
||||||
"/search"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base += "/$page"
|
|
||||||
|
|
||||||
if (isSortFilter) {
|
|
||||||
parseListing(
|
|
||||||
client.newCall(GET(baseUrl + base, headers))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.toSingle()
|
|
||||||
.await(Schedulers.io())
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val body = if (tagQuery != null) {
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("type", "advance")
|
|
||||||
.apply {
|
|
||||||
tagQuery.forEach {
|
|
||||||
add(it.first + "_" + it.second, if (it.third) "n" else "y")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("type", "search")
|
|
||||||
.add("needle", query)
|
|
||||||
}
|
|
||||||
val processRequest = POST(
|
|
||||||
"$baseUrl/content/process.php",
|
|
||||||
headers,
|
|
||||||
body = body.build()
|
|
||||||
)
|
|
||||||
val processResponse = nonRedirectingClientWithoutCookies.newCall(processRequest)
|
|
||||||
.asObservable()
|
|
||||||
.toSingle()
|
|
||||||
.await(Schedulers.io())
|
|
||||||
|
|
||||||
if (!processResponse.isRedirect) {
|
|
||||||
throw IllegalStateException("Unexpected process response code!")
|
|
||||||
}
|
|
||||||
|
|
||||||
val sessId = processResponse.headers("Set-Cookie").find {
|
|
||||||
it.startsWith("PHPSESSID")
|
|
||||||
} ?: throw IllegalStateException("Missing server session cookie!")
|
|
||||||
|
|
||||||
val response = clientWithoutCookies.newCall(
|
|
||||||
GET(
|
|
||||||
baseUrl + base,
|
|
||||||
headersBuilder()
|
|
||||||
.set("Cookie", BASE_COOKIES + " " + sessId.substringBefore(';'))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.toSingle()
|
|
||||||
.await(Schedulers.io())
|
|
||||||
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val manga = doc.select(".browseDescription").map {
|
|
||||||
SManga.create().apply {
|
|
||||||
val first = it.child(0)
|
|
||||||
url = first.attr("href")
|
|
||||||
title = first.attr("title").substringAfter('\'').removeSuffix("'").replace('_', ' ')
|
|
||||||
thumbnail_url = HBrowseSearchMetadata.guessThumbnailUrl(url.substring(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
|
|
||||||
MangasPage(
|
|
||||||
manga,
|
|
||||||
hasNextPage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.asSingle(GlobalScope.coroutineContext)
|
|
||||||
).toObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collection must be sorted and cannot be sorted
|
|
||||||
private fun List<String>.nearest(string: String, maxDist: Double = Int.MAX_VALUE.toDouble()): Pair<String, Double> {
|
|
||||||
val idx = binarySearch(string)
|
|
||||||
return if (idx < 0) {
|
|
||||||
val l = Levenshtein()
|
|
||||||
var minSoFar = maxDist
|
|
||||||
var minIndexSoFar = 0
|
|
||||||
forEachIndexed { index, s ->
|
|
||||||
val d = l.distance(string, s, ceil(minSoFar).toInt())
|
|
||||||
if (d < minSoFar) {
|
|
||||||
minSoFar = d
|
|
||||||
minIndexSoFar = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get(minIndexSoFar) to minSoFar
|
|
||||||
} else {
|
|
||||||
get(idx) to 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun searchMangaParse(response: Response) = parseListing(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for latest manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse/title/date/DESC/$page", headers)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesParse(response: Response) = parseListing(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the details of a manga.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
throw UnsupportedOperationException("Should not be called!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
|
override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
|
||||||
val tables = parseIntoTables(input)
|
val tables = parseIntoTables(input)
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
hbId = Uri.parse(input.location()).pathSegments.first().toLong()
|
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
|
||||||
|
|
||||||
|
hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong()
|
||||||
|
|
||||||
tags.clear()
|
tags.clear()
|
||||||
(tables[""]!! + tables["categories"]!!).forEach { (k, v) ->
|
((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) ->
|
||||||
when (val lowercaseNs = k.toLowerCase()) {
|
when (val lowercaseNs = k.toLowerCase()) {
|
||||||
"title" -> title = v.text()
|
"title" -> title = v.text()
|
||||||
"length" -> length = v.text().substringBefore(" ").toInt()
|
"length" -> length = v.text().substringBefore(" ").toInt()
|
||||||
@@ -340,35 +67,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param manga the manga to be updated.
|
|
||||||
*/
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of chapters.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
return parseIntoTables(response.asJsoup())["read manga online"]?.map { (key, value) ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = value.selectFirst(".listLink").attr("href")
|
|
||||||
|
|
||||||
name = key
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
|
private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
|
||||||
return doc.select("#main > .listTable").map { ele ->
|
return doc.select("#main > .listTable").map { ele ->
|
||||||
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
|
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
|
||||||
@@ -378,602 +76,16 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of pages.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val basePath = listOf("data") + response.request.url.pathSegments
|
|
||||||
val scripts = doc.getElementsByTag("script").map { it.data() }
|
|
||||||
for (script in scripts) {
|
|
||||||
val totalPages = TOTAL_PAGES_REGEX.find(script)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
|
||||||
?: continue
|
|
||||||
val pageList = PAGE_LIST_REGEX.find(script)?.groupValues?.getOrNull(1) ?: continue
|
|
||||||
|
|
||||||
return JsonParser.parseString(pageList).array.take(totalPages).map {
|
|
||||||
it.string
|
|
||||||
}.mapIndexed { index, pageName ->
|
|
||||||
Page(
|
|
||||||
index,
|
|
||||||
pageName,
|
|
||||||
"$baseUrl/${basePath.joinToString("/")}/$pageName"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
class HelpFilter : Filter.HelpDialog(
|
|
||||||
"Usage instructions",
|
|
||||||
markdown =
|
|
||||||
"""
|
|
||||||
### Modes
|
|
||||||
There are three available filter modes:
|
|
||||||
- Text search
|
|
||||||
- Tag search
|
|
||||||
- Sort mode
|
|
||||||
|
|
||||||
You can only use a single mode at a time. Switch between the text and tag search modes using the dropdown menu. Switch to sorting mode by selecting a sorting option.
|
|
||||||
|
|
||||||
### Text search
|
|
||||||
Search for galleries by title, artist or origin.
|
|
||||||
|
|
||||||
### Tag search
|
|
||||||
Search for galleries by tag (e.g. search for a specific genre, type, setting, etc). Uses nhentai/e-hentai syntax. Refer to the "Search" section on [this page](https://nhentai.net/info/) for more information.
|
|
||||||
|
|
||||||
### Sort mode
|
|
||||||
View a list of all galleries sorted by a specific parameter. Exit sorting mode by resetting the filters using the reset button near the bottom of the screen.
|
|
||||||
|
|
||||||
### Tag list
|
|
||||||
""".trimIndent() + "\n$TAGS_AS_MARKDOWN"
|
|
||||||
)
|
|
||||||
|
|
||||||
class ModeFilter : Filter.Select<String>(
|
|
||||||
"Mode",
|
|
||||||
arrayOf(
|
|
||||||
"Text search",
|
|
||||||
"Tag search"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
class SortFilter : Filter.Sort("Sort", SORT_OPTIONS.map { it.second }.toTypedArray()) {
|
|
||||||
companion object {
|
|
||||||
// internal to display
|
|
||||||
val SORT_OPTIONS = listOf(
|
|
||||||
"length" to "Length",
|
|
||||||
"date" to "Date added",
|
|
||||||
"rank" to "Rank"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
HelpFilter(),
|
|
||||||
ModeFilter(),
|
|
||||||
SortFilter()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the absolute url to the source image.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
throw UnsupportedOperationException("Should not be called!")
|
|
||||||
}
|
|
||||||
|
|
||||||
override val matchingHosts = listOf(
|
override val matchingHosts = listOf(
|
||||||
"www.hbrowse.com",
|
"www.hbrowse.com",
|
||||||
"hbrowse.com"
|
"hbrowse.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
return "$baseUrl/${uri.pathSegments.first()}"
|
return "/${uri.pathSegments.first()}/c00001/"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
||||||
private val PAGE_LIST_REGEX = Regex("list *= *(\\[.*]);")
|
return HBrowseDescriptionAdapter(controller)
|
||||||
private val TOTAL_PAGES_REGEX = Regex("totalPages *= *([0-9]*);")
|
|
||||||
|
|
||||||
private const val BASE_COOKIES = "thumbnails=1;"
|
|
||||||
|
|
||||||
private val NS_MAPPINGS = mapOf(
|
|
||||||
"set" to "setting",
|
|
||||||
"loc" to "setting",
|
|
||||||
"location" to "setting",
|
|
||||||
"fet" to "fetish",
|
|
||||||
"relation" to "relationship",
|
|
||||||
"male" to "malebody",
|
|
||||||
"female" to "femalebody",
|
|
||||||
"pos" to "position"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val ALL_TAGS = mapOf(
|
|
||||||
"genre" to listOf(
|
|
||||||
"action",
|
|
||||||
"adventure",
|
|
||||||
"anime",
|
|
||||||
"bizarre",
|
|
||||||
"comedy",
|
|
||||||
"drama",
|
|
||||||
"fantasy",
|
|
||||||
"gore",
|
|
||||||
"historic",
|
|
||||||
"horror",
|
|
||||||
"medieval",
|
|
||||||
"modern",
|
|
||||||
"myth",
|
|
||||||
"psychological",
|
|
||||||
"romance",
|
|
||||||
"school_life",
|
|
||||||
"scifi",
|
|
||||||
"supernatural",
|
|
||||||
"video_game",
|
|
||||||
"visual_novel"
|
|
||||||
),
|
|
||||||
"type" to listOf(
|
|
||||||
"anthology",
|
|
||||||
"bestiality",
|
|
||||||
"dandere",
|
|
||||||
"deredere",
|
|
||||||
"deviant",
|
|
||||||
"fully_colored",
|
|
||||||
"furry",
|
|
||||||
"futanari",
|
|
||||||
"gender_bender",
|
|
||||||
"guro",
|
|
||||||
"harem",
|
|
||||||
"incest",
|
|
||||||
"kuudere",
|
|
||||||
"lolicon",
|
|
||||||
"long_story",
|
|
||||||
"netorare",
|
|
||||||
"non-con",
|
|
||||||
"partly_colored",
|
|
||||||
"reverse_harem",
|
|
||||||
"ryona",
|
|
||||||
"short_story",
|
|
||||||
"shotacon",
|
|
||||||
"transgender",
|
|
||||||
"tsundere",
|
|
||||||
"uncensored",
|
|
||||||
"vanilla",
|
|
||||||
"yandere",
|
|
||||||
"yaoi",
|
|
||||||
"yuri"
|
|
||||||
),
|
|
||||||
"setting" to listOf(
|
|
||||||
"amusement_park",
|
|
||||||
"attic",
|
|
||||||
"automobile",
|
|
||||||
"balcony",
|
|
||||||
"basement",
|
|
||||||
"bath",
|
|
||||||
"beach",
|
|
||||||
"bedroom",
|
|
||||||
"cabin",
|
|
||||||
"castle",
|
|
||||||
"cave",
|
|
||||||
"church",
|
|
||||||
"classroom",
|
|
||||||
"deck",
|
|
||||||
"dining_room",
|
|
||||||
"doctors",
|
|
||||||
"dojo",
|
|
||||||
"doorway",
|
|
||||||
"dream",
|
|
||||||
"dressing_room",
|
|
||||||
"dungeon",
|
|
||||||
"elevator",
|
|
||||||
"festival",
|
|
||||||
"gym",
|
|
||||||
"haunted_building",
|
|
||||||
"hospital",
|
|
||||||
"hotel",
|
|
||||||
"hot_springs",
|
|
||||||
"kitchen",
|
|
||||||
"laboratory",
|
|
||||||
"library",
|
|
||||||
"living_room",
|
|
||||||
"locker_room",
|
|
||||||
"mansion",
|
|
||||||
"office",
|
|
||||||
"other",
|
|
||||||
"outdoor",
|
|
||||||
"outer_space",
|
|
||||||
"park",
|
|
||||||
"pool",
|
|
||||||
"prison",
|
|
||||||
"public",
|
|
||||||
"restaurant",
|
|
||||||
"restroom",
|
|
||||||
"roof",
|
|
||||||
"sauna",
|
|
||||||
"school",
|
|
||||||
"school_nurses_office",
|
|
||||||
"shower",
|
|
||||||
"shrine",
|
|
||||||
"storage_room",
|
|
||||||
"store",
|
|
||||||
"street",
|
|
||||||
"teachers_lounge",
|
|
||||||
"theater",
|
|
||||||
"tight_space",
|
|
||||||
"toilet",
|
|
||||||
"train",
|
|
||||||
"transit",
|
|
||||||
"virtual_reality",
|
|
||||||
"warehouse",
|
|
||||||
"wilderness"
|
|
||||||
),
|
|
||||||
"fetish" to listOf(
|
|
||||||
"androphobia",
|
|
||||||
"apron",
|
|
||||||
"assertive_girl",
|
|
||||||
"bikini",
|
|
||||||
"bloomers",
|
|
||||||
"breast_expansion",
|
|
||||||
"business_suit",
|
|
||||||
"chastity_device",
|
|
||||||
"chinese_dress",
|
|
||||||
"christmas",
|
|
||||||
"collar",
|
|
||||||
"corset",
|
|
||||||
"cosplay_(female)",
|
|
||||||
"cosplay_(male)",
|
|
||||||
"crossdressing_(female)",
|
|
||||||
"crossdressing_(male)",
|
|
||||||
"eye_patch",
|
|
||||||
"food",
|
|
||||||
"giantess",
|
|
||||||
"glasses",
|
|
||||||
"gothic_lolita",
|
|
||||||
"gyaru",
|
|
||||||
"gynophobia",
|
|
||||||
"high_heels",
|
|
||||||
"hot_pants",
|
|
||||||
"impregnation",
|
|
||||||
"kemonomimi",
|
|
||||||
"kimono",
|
|
||||||
"knee_high_socks",
|
|
||||||
"lab_coat",
|
|
||||||
"latex",
|
|
||||||
"leotard",
|
|
||||||
"lingerie",
|
|
||||||
"maid_outfit",
|
|
||||||
"mother_and_daughter",
|
|
||||||
"none",
|
|
||||||
"nonhuman_girl",
|
|
||||||
"olfactophilia",
|
|
||||||
"pregnant",
|
|
||||||
"rich_girl",
|
|
||||||
"school_swimsuit",
|
|
||||||
"shy_girl",
|
|
||||||
"sisters",
|
|
||||||
"sleeping_girl",
|
|
||||||
"sporty",
|
|
||||||
"stockings",
|
|
||||||
"strapon",
|
|
||||||
"student_uniform",
|
|
||||||
"swimsuit",
|
|
||||||
"tanned",
|
|
||||||
"tattoo",
|
|
||||||
"time_stop",
|
|
||||||
"twins_(coed)",
|
|
||||||
"twins_(female)",
|
|
||||||
"twins_(male)",
|
|
||||||
"uniform",
|
|
||||||
"wedding_dress"
|
|
||||||
),
|
|
||||||
"role" to listOf(
|
|
||||||
"alien",
|
|
||||||
"android",
|
|
||||||
"angel",
|
|
||||||
"athlete",
|
|
||||||
"bride",
|
|
||||||
"bunnygirl",
|
|
||||||
"cheerleader",
|
|
||||||
"delinquent",
|
|
||||||
"demon",
|
|
||||||
"doctor",
|
|
||||||
"dominatrix",
|
|
||||||
"escort",
|
|
||||||
"foreigner",
|
|
||||||
"ghost",
|
|
||||||
"housewife",
|
|
||||||
"idol",
|
|
||||||
"magical_girl",
|
|
||||||
"maid",
|
|
||||||
"mamono",
|
|
||||||
"massagist",
|
|
||||||
"miko",
|
|
||||||
"mythical_being",
|
|
||||||
"neet",
|
|
||||||
"nekomimi",
|
|
||||||
"newlywed",
|
|
||||||
"ninja",
|
|
||||||
"normal",
|
|
||||||
"nun",
|
|
||||||
"nurse",
|
|
||||||
"office_lady",
|
|
||||||
"other",
|
|
||||||
"police",
|
|
||||||
"priest",
|
|
||||||
"princess",
|
|
||||||
"queen",
|
|
||||||
"school_nurse",
|
|
||||||
"scientist",
|
|
||||||
"sorcerer",
|
|
||||||
"student",
|
|
||||||
"succubus",
|
|
||||||
"teacher",
|
|
||||||
"tomboy",
|
|
||||||
"tutor",
|
|
||||||
"waitress",
|
|
||||||
"warrior",
|
|
||||||
"witch"
|
|
||||||
),
|
|
||||||
"relationship" to listOf(
|
|
||||||
"acquaintance",
|
|
||||||
"anothers_daughter",
|
|
||||||
"anothers_girlfriend",
|
|
||||||
"anothers_mother",
|
|
||||||
"anothers_sister",
|
|
||||||
"anothers_wife",
|
|
||||||
"aunt",
|
|
||||||
"babysitter",
|
|
||||||
"childhood_friend",
|
|
||||||
"classmate",
|
|
||||||
"cousin",
|
|
||||||
"customer",
|
|
||||||
"daughter",
|
|
||||||
"daughter-in-law",
|
|
||||||
"employee",
|
|
||||||
"employer",
|
|
||||||
"enemy",
|
|
||||||
"fiance",
|
|
||||||
"friend",
|
|
||||||
"friends_daughter",
|
|
||||||
"friends_girlfriend",
|
|
||||||
"friends_mother",
|
|
||||||
"friends_sister",
|
|
||||||
"friends_wife",
|
|
||||||
"girlfriend",
|
|
||||||
"landlord",
|
|
||||||
"manager",
|
|
||||||
"master",
|
|
||||||
"mother",
|
|
||||||
"mother-in-law",
|
|
||||||
"neighbor",
|
|
||||||
"niece",
|
|
||||||
"none",
|
|
||||||
"older_sister",
|
|
||||||
"patient",
|
|
||||||
"pet",
|
|
||||||
"physician",
|
|
||||||
"relative",
|
|
||||||
"relatives_friend",
|
|
||||||
"relatives_girlfriend",
|
|
||||||
"relatives_wife",
|
|
||||||
"servant",
|
|
||||||
"server",
|
|
||||||
"sister-in-law",
|
|
||||||
"slave",
|
|
||||||
"stepdaughter",
|
|
||||||
"stepmother",
|
|
||||||
"stepsister",
|
|
||||||
"stranger",
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"tutee",
|
|
||||||
"tutor",
|
|
||||||
"twin",
|
|
||||||
"underclassman",
|
|
||||||
"upperclassman",
|
|
||||||
"wife",
|
|
||||||
"workmate",
|
|
||||||
"younger_sister"
|
|
||||||
),
|
|
||||||
"maleBody" to listOf(
|
|
||||||
"adult",
|
|
||||||
"animal",
|
|
||||||
"animal_ears",
|
|
||||||
"bald",
|
|
||||||
"beard",
|
|
||||||
"dark_skin",
|
|
||||||
"elderly",
|
|
||||||
"exaggerated_penis",
|
|
||||||
"fat",
|
|
||||||
"furry",
|
|
||||||
"goatee",
|
|
||||||
"hairy",
|
|
||||||
"half_animal",
|
|
||||||
"horns",
|
|
||||||
"large_penis",
|
|
||||||
"long_hair",
|
|
||||||
"middle_age",
|
|
||||||
"monster",
|
|
||||||
"muscular",
|
|
||||||
"mustache",
|
|
||||||
"none",
|
|
||||||
"short",
|
|
||||||
"short_hair",
|
|
||||||
"skinny",
|
|
||||||
"small_penis",
|
|
||||||
"tail",
|
|
||||||
"tall",
|
|
||||||
"tanned",
|
|
||||||
"tan_line",
|
|
||||||
"teenager",
|
|
||||||
"wings",
|
|
||||||
"young"
|
|
||||||
),
|
|
||||||
"femaleBody" to listOf(
|
|
||||||
"adult",
|
|
||||||
"animal_ears",
|
|
||||||
"bald",
|
|
||||||
"big_butt",
|
|
||||||
"chubby",
|
|
||||||
"dark_skin",
|
|
||||||
"elderly",
|
|
||||||
"elf_ears",
|
|
||||||
"exaggerated_breasts",
|
|
||||||
"fat",
|
|
||||||
"furry",
|
|
||||||
"hairy",
|
|
||||||
"hair_bun",
|
|
||||||
"half_animal",
|
|
||||||
"halo",
|
|
||||||
"hime_cut",
|
|
||||||
"horns",
|
|
||||||
"large_breasts",
|
|
||||||
"long_hair",
|
|
||||||
"middle_age",
|
|
||||||
"monster_girl",
|
|
||||||
"muscular",
|
|
||||||
"none",
|
|
||||||
"pigtails",
|
|
||||||
"ponytail",
|
|
||||||
"short",
|
|
||||||
"short_hair",
|
|
||||||
"skinny",
|
|
||||||
"small_breasts",
|
|
||||||
"tail",
|
|
||||||
"tall",
|
|
||||||
"tanned",
|
|
||||||
"tan_line",
|
|
||||||
"teenager",
|
|
||||||
"twintails",
|
|
||||||
"wings",
|
|
||||||
"young"
|
|
||||||
),
|
|
||||||
"grouping" to listOf(
|
|
||||||
"foursome_(1_female)",
|
|
||||||
"foursome_(1_male)",
|
|
||||||
"foursome_(mixed)",
|
|
||||||
"foursome_(only_female)",
|
|
||||||
"one_on_one",
|
|
||||||
"one_on_one_(2_females)",
|
|
||||||
"one_on_one_(2_males)",
|
|
||||||
"orgy_(1_female)",
|
|
||||||
"orgy_(1_male)",
|
|
||||||
"orgy_(mainly_female)",
|
|
||||||
"orgy_(mainly_male)",
|
|
||||||
"orgy_(mixed)",
|
|
||||||
"orgy_(only_female)",
|
|
||||||
"orgy_(only_male)",
|
|
||||||
"solo_(female)",
|
|
||||||
"solo_(male)",
|
|
||||||
"threesome_(1_female)",
|
|
||||||
"threesome_(1_male)",
|
|
||||||
"threesome_(only_female)",
|
|
||||||
"threesome_(only_male)"
|
|
||||||
),
|
|
||||||
"scene" to listOf(
|
|
||||||
"adultery",
|
|
||||||
"ahegao",
|
|
||||||
"anal_(female)",
|
|
||||||
"anal_(male)",
|
|
||||||
"aphrodisiac",
|
|
||||||
"armpit_sex",
|
|
||||||
"asphyxiation",
|
|
||||||
"blackmail",
|
|
||||||
"blowjob",
|
|
||||||
"bondage",
|
|
||||||
"breast_feeding",
|
|
||||||
"breast_sucking",
|
|
||||||
"bukkake",
|
|
||||||
"cheating_(female)",
|
|
||||||
"cheating_(male)",
|
|
||||||
"chikan",
|
|
||||||
"clothed_sex",
|
|
||||||
"consensual",
|
|
||||||
"cunnilingus",
|
|
||||||
"defloration",
|
|
||||||
"discipline",
|
|
||||||
"dominance",
|
|
||||||
"double_penetration",
|
|
||||||
"drunk",
|
|
||||||
"enema",
|
|
||||||
"exhibitionism",
|
|
||||||
"facesitting",
|
|
||||||
"fingering_(female)",
|
|
||||||
"fingering_(male)",
|
|
||||||
"fisting",
|
|
||||||
"footjob",
|
|
||||||
"grinding",
|
|
||||||
"groping",
|
|
||||||
"handjob",
|
|
||||||
"humiliation",
|
|
||||||
"hypnosis",
|
|
||||||
"intercrural",
|
|
||||||
"interracial_sex",
|
|
||||||
"interspecies_sex",
|
|
||||||
"lactation",
|
|
||||||
"lotion",
|
|
||||||
"masochism",
|
|
||||||
"masturbation",
|
|
||||||
"mind_break",
|
|
||||||
"nonhuman",
|
|
||||||
"orgy",
|
|
||||||
"paizuri",
|
|
||||||
"phone_sex",
|
|
||||||
"props",
|
|
||||||
"rape",
|
|
||||||
"reverse_rape",
|
|
||||||
"rimjob",
|
|
||||||
"sadism",
|
|
||||||
"scat",
|
|
||||||
"sex_toys",
|
|
||||||
"spanking",
|
|
||||||
"squirt",
|
|
||||||
"submission",
|
|
||||||
"sumata",
|
|
||||||
"swingers",
|
|
||||||
"tentacles",
|
|
||||||
"voyeurism",
|
|
||||||
"watersports",
|
|
||||||
"x-ray_blowjob",
|
|
||||||
"x-ray_sex"
|
|
||||||
),
|
|
||||||
"position" to listOf(
|
|
||||||
"69",
|
|
||||||
"acrobat",
|
|
||||||
"arch",
|
|
||||||
"bodyguard",
|
|
||||||
"butterfly",
|
|
||||||
"cowgirl",
|
|
||||||
"dancer",
|
|
||||||
"deck_chair",
|
|
||||||
"deep_stick",
|
|
||||||
"doggy",
|
|
||||||
"drill",
|
|
||||||
"ex_sex",
|
|
||||||
"jockey",
|
|
||||||
"lap_dance",
|
|
||||||
"leg_glider",
|
|
||||||
"lotus",
|
|
||||||
"mastery",
|
|
||||||
"missionary",
|
|
||||||
"none",
|
|
||||||
"other",
|
|
||||||
"pile_driver",
|
|
||||||
"prison_guard",
|
|
||||||
"reverse_piggyback",
|
|
||||||
"rodeo",
|
|
||||||
"spoons",
|
|
||||||
"standing",
|
|
||||||
"teaspoons",
|
|
||||||
"unusual",
|
|
||||||
"victory"
|
|
||||||
)
|
|
||||||
).mapValues { it.value.sorted() }
|
|
||||||
|
|
||||||
private val TAGS_AS_MARKDOWN = ALL_TAGS.map { (ns, values) ->
|
|
||||||
"#### $ns\n" + values.map { "- $it" }.joinToString("\n")
|
|
||||||
}.joinToString("\n\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
@@ -8,18 +9,20 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.metadata.metadata.HentaiCafeSearchMetadata
|
import exh.metadata.metadata.HentaiCafeSearchMetadata
|
||||||
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
|
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class HentaiCafe(delegate: HttpSource) :
|
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<HentaiCafeSearchMetadata, Document>,
|
LewdSource<HentaiCafeSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
@@ -34,7 +37,7 @@ class HentaiCafe(delegate: HttpSource) :
|
|||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,4 +113,8 @@ class HentaiCafe(delegate: HttpSource) :
|
|||||||
"https://hentai.cafe/$lcFirstPathSegment"
|
"https://hentai.cafe/$lcFirstPathSegment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
|
||||||
|
return HentaiCafeDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
@@ -8,17 +9,20 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.metadata.metadata.PururinSearchMetadata
|
import exh.metadata.metadata.PururinSearchMetadata
|
||||||
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
|
import exh.ui.metadata.adapters.PururinDescriptionAdapter
|
||||||
import exh.util.dropBlank
|
import exh.util.dropBlank
|
||||||
import exh.util.trimAll
|
import exh.util.trimAll
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class Pururin(delegate: HttpSource) :
|
class Pururin(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<PururinSearchMetadata, Document>,
|
LewdSource<PururinSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
@@ -38,7 +42,7 @@ class Pururin(delegate: HttpSource) :
|
|||||||
"$baseUrl/gallery/$trimmedIdQuery/-"
|
"$baseUrl/gallery/$trimmedIdQuery/-"
|
||||||
} else query
|
} else query
|
||||||
|
|
||||||
return urlImportFetchSearchManga(newQuery) {
|
return urlImportFetchSearchManga(context, newQuery) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,10 +92,11 @@ class Pururin(delegate: HttpSource) :
|
|||||||
else -> {
|
else -> {
|
||||||
value.select("a").forEach { link ->
|
value.select("a").forEach { link ->
|
||||||
val searchUrl = Uri.parse(link.attr("href"))
|
val searchUrl = Uri.parse(link.attr("href"))
|
||||||
|
val namespace = searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2]
|
||||||
tags += RaisedTag(
|
tags += RaisedTag(
|
||||||
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
|
namespace,
|
||||||
searchUrl.lastPathSegment!!.substringBefore("."),
|
searchUrl.lastPathSegment!!.substringBefore("."),
|
||||||
PururinSearchMetadata.TAG_TYPE_DEFAULT
|
if (namespace != PururinSearchMetadata.TAG_NAMESPACE_CATEGORY) PururinSearchMetadata.TAG_TYPE_DEFAULT else TAG_TYPE_VIRTUAL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,4 +113,8 @@ class Pururin(delegate: HttpSource) :
|
|||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
|
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter {
|
||||||
|
return PururinDescriptionAdapter(controller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
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.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
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.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
|
import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
|
||||||
|
import exh.util.dropBlank
|
||||||
|
import exh.util.trimAll
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class Tsumino(delegate: HttpSource) :
|
class Tsumino(delegate: HttpSource, val context: Context) :
|
||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
LewdSource<TsuminoSearchMetadata, Document>,
|
LewdSource<TsuminoSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
@@ -27,13 +33,13 @@ class Tsumino(delegate: HttpSource) :
|
|||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase(Locale.ROOT) ?: return null
|
||||||
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
|
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -57,9 +63,12 @@ class Tsumino(delegate: HttpSource) :
|
|||||||
title = it.trim()
|
title = it.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let {
|
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { artistString ->
|
||||||
tags.add(RaisedTag("artist", it, TAG_TYPE_VIRTUAL))
|
artistString.split("|").trimAll().dropBlank().forEach {
|
||||||
artist = it
|
tags.add(RaisedTag("artist", it, TAG_TYPE_DEFAULT))
|
||||||
|
}
|
||||||
|
tags.add(RaisedTag("artist", artistString, TAG_TYPE_VIRTUAL))
|
||||||
|
artist = artistString
|
||||||
}
|
}
|
||||||
|
|
||||||
input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
|
input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
|
||||||
@@ -76,6 +85,12 @@ class Tsumino(delegate: HttpSource) :
|
|||||||
|
|
||||||
input.getElementById("Rating")?.text()?.let {
|
input.getElementById("Rating")?.text()?.let {
|
||||||
ratingString = it.trim()
|
ratingString = it.trim()
|
||||||
|
val ratingString = ratingString
|
||||||
|
if (!ratingString.isNullOrBlank()) {
|
||||||
|
averageRating = RATING_FLOAT_REGEX.find(ratingString)?.groups?.get(1)?.value?.toFloatOrNull()
|
||||||
|
userRatings = RATING_USERS_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull()
|
||||||
|
favorites = RATING_FAVORITES_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.getElementById("Category")?.children()?.first()?.text()?.let {
|
input.getElementById("Category")?.children()?.first()?.text()?.let {
|
||||||
@@ -85,18 +100,19 @@ class Tsumino(delegate: HttpSource) :
|
|||||||
|
|
||||||
input.getElementById("Collection")?.children()?.first()?.text()?.let {
|
input.getElementById("Collection")?.children()?.first()?.text()?.let {
|
||||||
collection = it.trim()
|
collection = it.trim()
|
||||||
|
tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT))
|
||||||
}
|
}
|
||||||
|
|
||||||
input.getElementById("Group")?.children()?.first()?.text()?.let {
|
input.getElementById("Group")?.children()?.first()?.text()?.let {
|
||||||
group = it.trim()
|
group = it.trim()
|
||||||
tags.add(RaisedTag("group", it, TAG_TYPE_VIRTUAL))
|
tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newParody = mutableListOf<String>()
|
val newParody = mutableListOf<String>()
|
||||||
input.getElementById("Parody")?.children()?.forEach {
|
input.getElementById("Parody")?.children()?.forEach {
|
||||||
val entry = it.text().trim()
|
val entry = it.text().trim()
|
||||||
newParody.add(entry)
|
newParody.add(entry)
|
||||||
tags.add(RaisedTag("parody", entry, TAG_TYPE_VIRTUAL))
|
tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT))
|
||||||
}
|
}
|
||||||
parody = newParody
|
parody = newParody
|
||||||
|
|
||||||
@@ -104,14 +120,14 @@ class Tsumino(delegate: HttpSource) :
|
|||||||
input.getElementById("Character")?.children()?.forEach {
|
input.getElementById("Character")?.children()?.forEach {
|
||||||
val entry = it.text().trim()
|
val entry = it.text().trim()
|
||||||
newCharacter.add(entry)
|
newCharacter.add(entry)
|
||||||
tags.add(RaisedTag("character", entry, TAG_TYPE_VIRTUAL))
|
tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT))
|
||||||
}
|
}
|
||||||
character = newCharacter
|
character = newCharacter
|
||||||
|
|
||||||
input.getElementById("Tag")?.children()?.let {
|
input.getElementById("Tag")?.children()?.let { tagElements ->
|
||||||
tags.addAll(
|
tags.addAll(
|
||||||
it.map {
|
tagElements.map {
|
||||||
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
|
RaisedTag("tags", it.text().trim(), TAG_TYPE_DEFAULT)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -125,6 +141,12 @@ class Tsumino(delegate: HttpSource) :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US)
|
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US)
|
||||||
private val ASP_NET_COOKIE_NAME = "ASP.NET_SessionId"
|
val RATING_FLOAT_REGEX = "([0-9].*) \\(".toRegex()
|
||||||
|
val RATING_USERS_REGEX = "\\(([0-9].*) users".toRegex()
|
||||||
|
val RATING_FAVORITES_REGEX = "/ ([0-9].*) favs".toRegex()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDescriptionAdapter(controller: MangaController): TsuminoDescriptionAdapter {
|
||||||
|
return TsuminoDescriptionAdapter(controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTitle() {
|
fun setTitle(title: String? = null) {
|
||||||
var parentController = parentController
|
var parentController = parentController
|
||||||
while (parentController != null) {
|
while (parentController != null) {
|
||||||
if (parentController is BaseController<*> && parentController.getTitle() != null) {
|
if (parentController is BaseController<*> && parentController.getTitle() != null) {
|
||||||
@@ -83,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
parentController = parentController.parentController
|
parentController = parentController.parentController
|
||||||
}
|
}
|
||||||
|
|
||||||
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
(activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Controller.instance(): String {
|
private fun Controller.instance(): String {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Controller.withFadeTransaction(): RouterTransaction {
|
fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction {
|
||||||
return RouterTransaction.with(this)
|
return RouterTransaction.with(this)
|
||||||
.pushChangeHandler(FadeChangeHandler())
|
.pushChangeHandler(FadeChangeHandler(duration))
|
||||||
.popChangeHandler(FadeChangeHandler())
|
.popChangeHandler(FadeChangeHandler(duration))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
|
|||||||
/**
|
/**
|
||||||
* Dismiss the dialog and pop this controller
|
* Dismiss the dialog and pop this controller
|
||||||
*/
|
*/
|
||||||
fun dismissDialog() {
|
private fun dismissDialog() {
|
||||||
if (dismissed) {
|
if (dismissed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
|
||||||
|
interface FabController {
|
||||||
|
|
||||||
|
fun configureFab(fab: ExtendedFloatingActionButton) {}
|
||||||
|
|
||||||
|
fun cleanupFab(fab: ExtendedFloatingActionButton) {}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ class BrowseController :
|
|||||||
override fun configureTabs(tabs: TabLayout) {
|
override fun configureTabs(tabs: TabLayout) {
|
||||||
with(tabs) {
|
with(tabs) {
|
||||||
tabGravity = TabLayout.GRAVITY_FILL
|
tabGravity = TabLayout.GRAVITY_FILL
|
||||||
tabMode = TabLayout.MODE_AUTO
|
tabMode = TabLayout.MODE_FIXED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -1,10 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||||
@@ -22,11 +23,10 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
|||||||
for (i in 0 until childCount - 1) {
|
for (i in 0 until childCount - 1) {
|
||||||
val child = parent.getChildAt(i)
|
val child = parent.getChildAt(i)
|
||||||
val holder = parent.getChildViewHolder(child)
|
val holder = parent.getChildViewHolder(child)
|
||||||
if (holder is SourceHolder &&
|
if (holder is SourceListItem &&
|
||||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder
|
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceListItem
|
||||||
) {
|
) {
|
||||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
val top = child.bottom + child.marginBottom
|
||||||
val top = child.bottom + params.bottomMargin
|
|
||||||
val bottom = top + divider.intrinsicHeight
|
val bottom = top + divider.intrinsicHeight
|
||||||
val left = parent.paddingStart + holder.margin
|
val left = parent.paddingStart + holder.margin
|
||||||
val right = parent.width - parent.paddingEnd - holder.margin
|
val right = parent.width - parent.paddingEnd - holder.margin
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
|
||||||
|
interface SourceListItem : SlicedHolder
|
||||||
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@@ -75,7 +76,7 @@ open class ExtensionController :
|
|||||||
// Create recycler and set adapter.
|
// Create recycler and set adapter.
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +130,9 @@ open class ExtensionController :
|
|||||||
val searchView = searchItem.actionView as SearchView
|
val searchView = searchItem.actionView as SearchView
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
searchView.maxWidth = Int.MAX_VALUE
|
||||||
|
|
||||||
|
// Fixes problem with the overflow icon showing up in lieu of search
|
||||||
|
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||||
|
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
searchItem.expandActionView()
|
searchItem.expandActionView()
|
||||||
searchView.setQuery(query, true)
|
searchView.setQuery(query, true)
|
||||||
@@ -142,9 +146,6 @@ open class ExtensionController :
|
|||||||
drawExtensions()
|
drawExtensions()
|
||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
// Fixes problem with the overflow icon showing up in lieu of search
|
|
||||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
|
|||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val divider: Drawable
|
|
||||||
|
|
||||||
init {
|
|
||||||
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
|
||||||
divider = a.getDrawable(0)!!
|
|
||||||
a.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
val childCount = parent.childCount
|
|
||||||
for (i in 0 until childCount - 1) {
|
|
||||||
val child = parent.getChildAt(i)
|
|
||||||
val holder = parent.getChildViewHolder(child)
|
|
||||||
if (holder is ExtensionHolder &&
|
|
||||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder
|
|
||||||
) {
|
|
||||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
|
||||||
val top = child.bottom + params.bottomMargin
|
|
||||||
val bottom = top + divider.intrinsicHeight
|
|
||||||
val left = parent.paddingStart + holder.margin
|
|
||||||
val right = parent.width - parent.paddingEnd - holder.margin
|
|
||||||
|
|
||||||
divider.setBounds(left, top, right, bottom)
|
|
||||||
divider.draw(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
|||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.extension_card_item.card
|
import kotlinx.android.synthetic.main.extension_card_item.card
|
||||||
@@ -22,6 +23,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
|
|
||||||
override val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
@@ -48,7 +50,9 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
|||||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
||||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
|
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
|
||||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
|
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
|
||||||
|
// SY -->
|
||||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase()
|
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase()
|
||||||
|
// SY <--
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +95,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
|||||||
setText(R.string.ext_update)
|
setText(R.string.ext_update)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
// SY -->
|
||||||
if (extension.sources.any { it is ConfigurableSource }) {
|
if (extension.sources.any { it is ConfigurableSource }) {
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
text = context.getString(R.string.action_settings) + "+"
|
text = context.getString(R.string.action_settings) + "+"
|
||||||
} else {
|
} else {
|
||||||
setText(R.string.action_settings)
|
setText(R.string.action_settings)
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (extension is Extension.Untrusted) {
|
} else if (extension is Extension.Untrusted) {
|
||||||
|
|||||||
+17
-8
@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -23,6 +26,8 @@ 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.EmptyPreferenceDataStore
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
|
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
@@ -180,6 +185,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_enable_all -> toggleAllSources(true)
|
R.id.action_enable_all -> toggleAllSources(true)
|
||||||
R.id.action_disable_all -> toggleAllSources(false)
|
R.id.action_disable_all -> toggleAllSources(false)
|
||||||
|
R.id.action_open_in_settings -> openInSettings()
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@@ -193,15 +199,18 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleSource(source: Source, enable: Boolean) {
|
private fun toggleSource(source: Source, enable: Boolean) {
|
||||||
val current = preferences.disabledSources().get()
|
if (enable) {
|
||||||
|
preferences.disabledSources() -= source.id.toString()
|
||||||
|
} else {
|
||||||
|
preferences.disabledSources() += source.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preferences.disabledSources().set(
|
private fun openInSettings() {
|
||||||
if (enable) {
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
current - source.id.toString()
|
data = Uri.fromParts("package", presenter.pkgName, null)
|
||||||
} else {
|
}
|
||||||
current + source.id.toString()
|
startActivity(intent)
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Source.isEnabled(): Boolean {
|
private fun Source.isEnabled(): Boolean {
|
||||||
|
|||||||
+4
-4
@@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
|
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
|
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -49,18 +49,18 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
|||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
if (extension.isObsolete) {
|
if (extension.isObsolete) {
|
||||||
binding.extensionWarningBanner.visible()
|
binding.extensionWarningBanner.isVisible = true
|
||||||
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
|
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extension.isUnofficial) {
|
if (extension.isUnofficial) {
|
||||||
binding.extensionWarningBanner.visible()
|
binding.extensionWarningBanner.isVisible = true
|
||||||
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (extension.isRedundant) {
|
if (extension.isRedundant) {
|
||||||
binding.extensionWarningBanner.visible()
|
binding.extensionWarningBanner.isVisible = true
|
||||||
binding.extensionWarningBanner.setText(R.string.redundant_extension_message)
|
binding.extensionWarningBanner.setText(R.string.redundant_extension_message)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
||||||
@@ -72,11 +71,7 @@ open class LatestController :
|
|||||||
*/
|
*/
|
||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
// Open MangaController.
|
// Open MangaController.
|
||||||
if (presenter.preferences.eh_useNewMangaInterface().get()) {
|
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
|
||||||
parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction())
|
|
||||||
} else {
|
|
||||||
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.latest
|
package eu.kanade.tachiyomi.ui.browse.latest
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
import kotlinx.android.synthetic.main.latest_controller_card.no_results_found
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import kotlinx.android.synthetic.main.latest_controller_card.progress
|
import kotlinx.android.synthetic.main.latest_controller_card.progress
|
||||||
import kotlinx.android.synthetic.main.latest_controller_card.recycler
|
import kotlinx.android.synthetic.main.latest_controller_card.recycler
|
||||||
import kotlinx.android.synthetic.main.latest_controller_card.source_card
|
import kotlinx.android.synthetic.main.latest_controller_card.source_card
|
||||||
@@ -61,16 +61,16 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
results == null -> {
|
results == null -> {
|
||||||
progress.visible()
|
progress.isVisible = true
|
||||||
showHolder()
|
showResultsHolder()
|
||||||
}
|
}
|
||||||
results.isEmpty() -> {
|
results.isEmpty() -> {
|
||||||
progress.gone()
|
progress.isVisible = false
|
||||||
hideHolder()
|
showNoResults()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
progress.gone()
|
progress.isVisible = false
|
||||||
showHolder()
|
showResultsHolder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (results !== lastBoundResults) {
|
if (results !== lastBoundResults) {
|
||||||
@@ -105,13 +105,13 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showHolder() {
|
private fun showResultsHolder() {
|
||||||
title_wrapper.visible()
|
no_results_found.isVisible = false
|
||||||
source_card.visible()
|
source_card.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideHolder() {
|
private fun showNoResults() {
|
||||||
title_wrapper.gone()
|
no_results_found.isVisible = true
|
||||||
source_card.gone()
|
source_card.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-8
@@ -8,6 +8,7 @@ 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
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
@@ -15,8 +16,6 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import kotlinx.android.synthetic.main.migration_bottom_sheet.*
|
import kotlinx.android.synthetic.main.migration_bottom_sheet.*
|
||||||
import kotlinx.android.synthetic.main.migration_bottom_sheet.extra_search_param
|
import kotlinx.android.synthetic.main.migration_bottom_sheet.extra_search_param
|
||||||
import kotlinx.android.synthetic.main.migration_bottom_sheet.extra_search_param_text
|
import kotlinx.android.synthetic.main.migration_bottom_sheet.extra_search_param_text
|
||||||
@@ -88,13 +87,9 @@ class MigrationBottomSheetDialog(
|
|||||||
mig_tracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
mig_tracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
|
|
||||||
use_smart_search.bindToPreference(preferences.smartMigration())
|
use_smart_search.bindToPreference(preferences.smartMigration())
|
||||||
extra_search_param_text.gone()
|
extra_search_param_text.isVisible = false
|
||||||
extra_search_param.setOnCheckedChangeListener { _, isChecked ->
|
extra_search_param.setOnCheckedChangeListener { _, isChecked ->
|
||||||
if (isChecked) {
|
extra_search_param_text.isVisible = isChecked
|
||||||
extra_search_param_text.visible()
|
|
||||||
} else {
|
|
||||||
extra_search_param_text.gone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sourceGroup.bindToPreference(preferences.useSourceWithMost())
|
sourceGroup.bindToPreference(preferences.useSourceWithMost())
|
||||||
|
|
||||||
|
|||||||
+6
-8
@@ -29,7 +29,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
|||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
@@ -167,12 +166,12 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
val searchResult = if (useSmartSearch) {
|
val searchResult = if (useSmartSearch) {
|
||||||
smartSearchEngine.smartSearch(
|
smartSearchEngine.smartSearch(
|
||||||
source,
|
source,
|
||||||
mangaObj.title
|
mangaObj.originalTitle
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
smartSearchEngine.normalSearch(
|
smartSearchEngine.normalSearch(
|
||||||
source,
|
source,
|
||||||
mangaObj.title
|
mangaObj.originalTitle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +221,12 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
val searchResult = if (useSmartSearch) {
|
val searchResult = if (useSmartSearch) {
|
||||||
smartSearchEngine.smartSearch(
|
smartSearchEngine.smartSearch(
|
||||||
source,
|
source,
|
||||||
mangaObj.title
|
mangaObj.originalTitle
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
smartSearchEngine.normalSearch(
|
smartSearchEngine.normalSearch(
|
||||||
source,
|
source,
|
||||||
mangaObj.title
|
mangaObj.originalTitle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +426,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
private fun navigateOut() {
|
private fun navigateOut() {
|
||||||
if (migratingManga?.size == 1) {
|
if (migratingManga?.size == 1) {
|
||||||
launchUI {
|
launchUI {
|
||||||
val hasDetails = router.backstack.any { it.controller() is MangaController } || router.backstack.any { it.controller() is MangaAllInOneController }
|
val hasDetails = router.backstack.any { it.controller() is MangaController }
|
||||||
if (hasDetails) {
|
if (hasDetails) {
|
||||||
val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let {
|
val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let {
|
||||||
db.getManga(it).executeOnIO()
|
db.getManga(it).executeOnIO()
|
||||||
@@ -435,10 +434,9 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
if (manga != null) {
|
if (manga != null) {
|
||||||
val newStack = router.backstack.filter {
|
val newStack = router.backstack.filter {
|
||||||
it.controller() !is MangaController &&
|
it.controller() !is MangaController &&
|
||||||
it.controller() !is MangaAllInOneController &&
|
|
||||||
it.controller() !is MigrationListController &&
|
it.controller() !is MigrationListController &&
|
||||||
it.controller() !is PreMigrationController
|
it.controller() !is PreMigrationController
|
||||||
} + if (preferences.eh_useNewMangaInterface().get()) MangaAllInOneController(manga).withFadeTransaction() else MangaController(manga).withFadeTransaction()
|
} + MangaController(manga).withFadeTransaction()
|
||||||
router.setBackstack(newStack, FadeChangeHandler())
|
router.setBackstack(newStack, FadeChangeHandler())
|
||||||
return@launchUI
|
return@launchUI
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import java.util.Date
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -135,7 +136,10 @@ class MigrationProcessAdapter(
|
|||||||
// Update favorite status
|
// Update favorite status
|
||||||
if (replace) {
|
if (replace) {
|
||||||
prevManga.favorite = false
|
prevManga.favorite = false
|
||||||
|
manga.date_added = prevManga.date_added
|
||||||
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||||
|
} else {
|
||||||
|
manga.date_added = Date().time
|
||||||
}
|
}
|
||||||
manga.favorite = true
|
manga.favorite = true
|
||||||
|
|
||||||
|
|||||||
+19
-32
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@@ -9,20 +11,15 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
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.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.invisible
|
|
||||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import exh.MERGED_SOURCE_ID
|
import exh.MERGED_SOURCE_ID
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import kotlinx.android.synthetic.main.migration_manga_card.view.gradient
|
import kotlinx.android.synthetic.main.migration_manga_card.view.gradient
|
||||||
@@ -38,7 +35,6 @@ import kotlinx.android.synthetic.main.migration_process_item.migration_menu
|
|||||||
import kotlinx.android.synthetic.main.migration_process_item.skip_manga
|
import kotlinx.android.synthetic.main.migration_process_item.skip_manga
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
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
|
||||||
|
|
||||||
@@ -78,28 +74,19 @@ class MigrationProcessHolder(
|
|||||||
.attr.colorOnPrimary
|
.attr.colorOnPrimary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
migration_menu.invisible()
|
migration_menu.isInvisible = true
|
||||||
skip_manga.visible()
|
skip_manga.isVisible = true
|
||||||
migration_manga_card_to.resetManga()
|
migration_manga_card_to.resetManga()
|
||||||
if (manga != null) {
|
if (manga != null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
migration_manga_card_from.attachManga(manga, source)
|
migration_manga_card_from.attachManga(manga, source)
|
||||||
migration_manga_card_from.setOnClickListener {
|
migration_manga_card_from.setOnClickListener {
|
||||||
if (Injekt.get<PreferencesHelper>().eh_useNewMangaInterface().get()) {
|
adapter.controller.router.pushController(
|
||||||
adapter.controller.router.pushController(
|
MangaController(
|
||||||
MangaAllInOneController(
|
manga,
|
||||||
manga,
|
true
|
||||||
true
|
).withFadeTransaction()
|
||||||
).withFadeTransaction()
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
adapter.controller.router.pushController(
|
|
||||||
MangaController(
|
|
||||||
manga,
|
|
||||||
true
|
|
||||||
).withFadeTransaction()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,12 +123,12 @@ class MigrationProcessHolder(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
migration_manga_card_to.loading_group.gone()
|
migration_manga_card_to.loading_group.isVisible = false
|
||||||
migration_manga_card_to.title.text = view.context.applicationContext
|
migration_manga_card_to.title.text = view.context.applicationContext
|
||||||
.getString(R.string.no_alternatives_found)
|
.getString(R.string.no_alternatives_found)
|
||||||
}
|
}
|
||||||
migration_menu.visible()
|
migration_menu.isVisible = true
|
||||||
skip_manga.gone()
|
skip_manga.isVisible = false
|
||||||
adapter.sourceFinished()
|
adapter.sourceFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,18 +136,18 @@ class MigrationProcessHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun View.resetManga() {
|
private fun View.resetManga() {
|
||||||
loading_group.visible()
|
loading_group.isVisible = true
|
||||||
thumbnail.setImageDrawable(null)
|
thumbnail.setImageDrawable(null)
|
||||||
title.text = ""
|
title.text = ""
|
||||||
manga_source_label.text = ""
|
manga_source_label.text = ""
|
||||||
manga_chapters.text = ""
|
manga_chapters.text = ""
|
||||||
manga_chapters.gone()
|
manga_chapters.isVisible = false
|
||||||
manga_last_chapter_label.text = ""
|
manga_last_chapter_label.text = ""
|
||||||
migration_manga_card_to.setOnClickListener(null)
|
migration_manga_card_to.setOnClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun View.attachManga(manga: Manga, source: Source) {
|
private fun View.attachManga(manga: Manga, source: Source) {
|
||||||
loading_group.gone()
|
loading_group.isVisible = false
|
||||||
GlideApp.with(view.context.applicationContext)
|
GlideApp.with(view.context.applicationContext)
|
||||||
.load(manga.toMangaThumbnail())
|
.load(manga.toMangaThumbnail())
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
@@ -171,10 +158,10 @@ class MigrationProcessHolder(
|
|||||||
title.text = if (manga.title.isBlank()) {
|
title.text = if (manga.title.isBlank()) {
|
||||||
view.context.getString(R.string.unknown)
|
view.context.getString(R.string.unknown)
|
||||||
} else {
|
} else {
|
||||||
manga.title
|
manga.originalTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
gradient.visible()
|
gradient.isVisible = true
|
||||||
manga_source_label.text = if (source.id == MERGED_SOURCE_ID) {
|
manga_source_label.text = if (source.id == MERGED_SOURCE_ID) {
|
||||||
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
|
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
|
||||||
sourceManager.getOrStub(it.source).toString()
|
sourceManager.getOrStub(it.source).toString()
|
||||||
@@ -184,7 +171,7 @@ class MigrationProcessHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
manga_chapters.visible()
|
manga_chapters.isVisible = true
|
||||||
manga_chapters.text = mangaChapters.size.toString()
|
manga_chapters.text = mangaChapters.size.toString()
|
||||||
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
|
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MangaHolder(
|
|||||||
|
|
||||||
fun bind(item: MangaItem) {
|
fun bind(item: MangaItem) {
|
||||||
// Update the title of the manga.
|
// Update the title of the manga.
|
||||||
title.text = item.manga.title
|
title.text = item.manga.originalTitle
|
||||||
|
|
||||||
// Create thumbnail onclick to simulate long click
|
// Create thumbnail onclick to simulate long click
|
||||||
thumbnail.setOnClickListener {
|
thumbnail.setOnClickListener {
|
||||||
|
|||||||
+1
-1
@@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import exh.debug.DebugFunctions.sourceManager
|
import exh.debug.DebugFunctions.sourceManager
|
||||||
|
import java.util.Date
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@@ -34,7 +35,7 @@ class MigrationMangaPresenter(
|
|||||||
|
|
||||||
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
|
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
|
||||||
return library.filter { it.source == sourceId }
|
return library.filter { it.source == sourceId }
|
||||||
.sortedBy { it.title }
|
.sortedBy { it.originalTitle }
|
||||||
.map { MangaItem(it) }
|
.map { MangaItem(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +102,11 @@ class MigrationMangaPresenter(
|
|||||||
// Update favorite status
|
// Update favorite status
|
||||||
if (replace) {
|
if (replace) {
|
||||||
prevManga.favorite = false
|
prevManga.favorite = false
|
||||||
|
manga.date_added = prevManga.date_added
|
||||||
|
prevManga.date_added = 0
|
||||||
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||||
|
} else {
|
||||||
|
manga.date_added = Date().time
|
||||||
}
|
}
|
||||||
manga.favorite = true
|
manga.favorite = true
|
||||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
class SearchController(
|
class SearchController(
|
||||||
private var manga: Manga? = null,
|
private var manga: Manga? = null,
|
||||||
private var sources: List<CatalogueSource>? = null
|
private var sources: List<CatalogueSource>? = null
|
||||||
) : GlobalSearchController(manga?.title) {
|
) : GlobalSearchController(manga?.originalTitle) {
|
||||||
|
|
||||||
private var newManga: Manga? = null
|
private var newManga: Manga? = null
|
||||||
private var progress = 1
|
private var progress = 1
|
||||||
|
|||||||
+1
-1
@@ -12,9 +12,9 @@ import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_browse
|
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
||||||
|
|
||||||
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
|
|
||||||
override val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
@@ -23,15 +25,15 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
|||||||
override val viewToSlice: View
|
override val viewToSlice: View
|
||||||
get() = card
|
get() = card
|
||||||
|
|
||||||
|
// SY -->
|
||||||
init {
|
init {
|
||||||
source_latest.gone()
|
source_latest.isVisible = true
|
||||||
// SY -->
|
source_latest.text = view.context.getString(R.string.all)
|
||||||
source_browse.text = "All"
|
source_latest.setOnClickListener {
|
||||||
source_browse.setOnClickListener {
|
|
||||||
adapter.allClickListener?.onAllClick(bindingAdapterPosition)
|
adapter.allClickListener?.onAllClick(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun bind(item: SourceItem) {
|
fun bind(item: SourceItem) {
|
||||||
val source = item.source
|
val source = item.source
|
||||||
|
|||||||
@@ -22,26 +22,15 @@ class SourceAdapter(val controller: SourceController) :
|
|||||||
/**
|
/**
|
||||||
* Listener for browse item clicks.
|
* Listener for browse item clicks.
|
||||||
*/
|
*/
|
||||||
val browseClickListener: OnBrowseClickListener = controller
|
val clickListener: OnSourceClickListener = controller
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for latest item clicks.
|
|
||||||
*/
|
|
||||||
val latestClickListener: OnLatestClickListener = controller
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener which should be called when user clicks browse.
|
* Listener which should be called when user clicks browse.
|
||||||
* Note: Should only be handled by [SourceController]
|
* Note: Should only be handled by [SourceController]
|
||||||
*/
|
*/
|
||||||
interface OnBrowseClickListener {
|
interface OnSourceClickListener {
|
||||||
fun onBrowseClick(position: Int)
|
fun onBrowseClick(position: Int)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener which should be called when user clicks latest.
|
|
||||||
* Note: Should only be handled by [SourceController]
|
|
||||||
*/
|
|
||||||
interface OnLatestClickListener {
|
|
||||||
fun onLatestClick(position: Int)
|
fun onLatestClick(position: Int)
|
||||||
|
fun onPinClick(position: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -19,14 +20,18 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
|||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
@@ -45,16 +50,15 @@ import uy.kohesive.injekt.api.get
|
|||||||
/**
|
/**
|
||||||
* This controller shows and manages the different catalogues enabled by the user.
|
* This controller shows and manages the different catalogues enabled by the user.
|
||||||
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
|
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
|
||||||
* [SourceAdapter.OnBrowseClickListener] call function data on browse item click.
|
* [SourceAdapter.OnSourceClickListener] call function data on browse item click.
|
||||||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||||
*/
|
*/
|
||||||
class SourceController(bundle: Bundle? = null) :
|
class SourceController(bundle: Bundle? = null) :
|
||||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
SourceAdapter.OnBrowseClickListener,
|
SourceAdapter.OnSourceClickListener,
|
||||||
SourceAdapter.OnLatestClickListener,
|
/*SY -->*/ ChangeSourceCategoriesDialog.Listener /*SY <--*/ {
|
||||||
ChangeSourceCategoriesDialog.Listener {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
|
||||||
@@ -171,11 +175,16 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
val items = mutableListOf(
|
val items = mutableListOf(
|
||||||
Pair(
|
Pair(
|
||||||
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin),
|
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin),
|
||||||
{ pinSource(item.source, isPinned) }
|
{ toggleSourcePin(item.source) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (item.source !is LocalSource) {
|
if (item.source !is LocalSource) {
|
||||||
items.add(Pair(activity.getString(R.string.action_disable), { disableSource(item.source) }))
|
items.add(
|
||||||
|
Pair(
|
||||||
|
activity.getString(R.string.action_disable),
|
||||||
|
{ disableSource(item.source) }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -198,31 +207,21 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
MaterialDialog(activity)
|
SourceOptionsDialog(item, items).showDialog(router)
|
||||||
.title(text = item.source.name)
|
|
||||||
.listItems(
|
|
||||||
items = items.map { it.first },
|
|
||||||
waitForPositiveButton = false
|
|
||||||
) { dialog, which, _ ->
|
|
||||||
items[which].second()
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun disableSource(source: Source) {
|
private fun disableSource(source: Source) {
|
||||||
val current = preferences.disabledSources().get()
|
preferences.disabledSources() += source.id.toString()
|
||||||
preferences.disabledSources().set(current + source.id.toString())
|
|
||||||
|
|
||||||
presenter.updateSources()
|
presenter.updateSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pinSource(source: Source, isPinned: Boolean) {
|
private fun toggleSourcePin(source: Source) {
|
||||||
val current = preferences.pinnedSources().get()
|
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
||||||
if (isPinned) {
|
if (isPinned) {
|
||||||
preferences.pinnedSources().set(current - source.id.toString())
|
preferences.pinnedSources() -= source.id.toString()
|
||||||
} else {
|
} else {
|
||||||
preferences.pinnedSources().set(current + source.id.toString())
|
preferences.pinnedSources() += source.id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
presenter.updateSources()
|
presenter.updateSources()
|
||||||
@@ -230,16 +229,14 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun watchCatalogue(source: Source, isWatched: Boolean) {
|
private fun watchCatalogue(source: Source, isWatched: Boolean) {
|
||||||
val current = preferences.latestTabSources().get()
|
|
||||||
|
|
||||||
if (isWatched) {
|
if (isWatched) {
|
||||||
preferences.latestTabSources().set(current - source.id.toString())
|
preferences.latestTabSources() -= source.id.toString()
|
||||||
} else {
|
} else {
|
||||||
if (current.size + 1 !in 0..5) {
|
if (preferences.latestTabSources().get().size + 1 !in 0..5) {
|
||||||
applicationContext?.toast(R.string.too_many_watched)
|
applicationContext?.toast(R.string.too_many_watched)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
preferences.latestTabSources().set(current + source.id.toString())
|
preferences.latestTabSources() += source.id.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +303,14 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
openSource(item.source, LatestUpdatesController(item.source))
|
openSource(item.source, LatestUpdatesController(item.source))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when pin icon is clicked in [SourceAdapter]
|
||||||
|
*/
|
||||||
|
override fun onPinClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
|
toggleSourcePin(item.source)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a catalogue with the given controller.
|
* Opens a catalogue with the given controller.
|
||||||
*/
|
*/
|
||||||
@@ -389,6 +394,29 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
|
private lateinit var item: SourceItem
|
||||||
|
private lateinit var items: List<Pair<String, () -> Unit>>
|
||||||
|
|
||||||
|
constructor(item: SourceItem, items: List<Pair<String, () -> Unit>>) : this() {
|
||||||
|
this.item = item
|
||||||
|
this.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog(activity!!)
|
||||||
|
.title(text = item.source.toString())
|
||||||
|
.listItems(
|
||||||
|
items = items.map { it.first },
|
||||||
|
waitForPositiveButton = false
|
||||||
|
) { dialog, which, _ ->
|
||||||
|
items[which].second()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
|
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import androidx.preference.CheckBoxPreference
|
|||||||
import androidx.preference.PreferenceGroup
|
import androidx.preference.PreferenceGroup
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -18,7 +20,6 @@ import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory
|
|||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
|
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
|
||||||
import exh.source.BlacklistedSources
|
|
||||||
import java.util.TreeMap
|
import java.util.TreeMap
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@@ -34,9 +35,7 @@ class SourceFilterController : SettingsController() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val onlineSources by lazy {
|
private val onlineSources by lazy {
|
||||||
Injekt.get<SourceManager>().getOnlineSources().filter {
|
Injekt.get<SourceManager>().getVisibleOnlineSources()
|
||||||
it.id !in BlacklistedSources.HIDDEN_SOURCES
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var query = ""
|
private var query = ""
|
||||||
@@ -80,12 +79,11 @@ class SourceFilterController : SettingsController() {
|
|||||||
|
|
||||||
onChange { newValue ->
|
onChange { newValue ->
|
||||||
val checked = newValue as Boolean
|
val checked = newValue as Boolean
|
||||||
val current = preferences.enabledLanguages().get()
|
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
preferences.enabledLanguages().set(current - lang)
|
preferences.enabledLanguages() -= lang
|
||||||
removeAll()
|
removeAll()
|
||||||
} else {
|
} else {
|
||||||
preferences.enabledLanguages().set(current + lang)
|
preferences.enabledLanguages() += lang
|
||||||
addLanguageSources(this, sortedSources(sourcesByLang[lang]))
|
addLanguageSources(this, sortedSources(sourcesByLang[lang]))
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -147,15 +145,12 @@ class SourceFilterController : SettingsController() {
|
|||||||
|
|
||||||
onChange { newValue ->
|
onChange { newValue ->
|
||||||
val checked = newValue as Boolean
|
val checked = newValue as Boolean
|
||||||
val current = preferences.disabledSources().get()
|
|
||||||
|
|
||||||
preferences.disabledSources().set(
|
if (checked) {
|
||||||
if (checked) {
|
preferences.disabledSources() -= id
|
||||||
current - id
|
} else {
|
||||||
} else {
|
preferences.disabledSources() += id
|
||||||
current + id
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
group.removeAll()
|
group.removeAll()
|
||||||
addLanguageSources(group, sortedSources(sources))
|
addLanguageSources(group, sortedSources(sources))
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
|
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_browse
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.pin
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
||||||
|
|
||||||
class SourceHolder(view: View, override val adapter: SourceAdapter /* SY --> */, val showButtons: Boolean /* SY <-- */) :
|
class SourceHolder(private val view: View, override val adapter: SourceAdapter /* SY --> */, private val showButtons: Boolean /* SY <-- */) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
|
|
||||||
override val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
@@ -27,18 +30,17 @@ class SourceHolder(view: View, override val adapter: SourceAdapter /* SY --> */,
|
|||||||
get() = card
|
get() = card
|
||||||
|
|
||||||
init {
|
init {
|
||||||
source_browse.setOnClickListener {
|
source_latest.setOnClickListener {
|
||||||
adapter.browseClickListener.onBrowseClick(bindingAdapterPosition)
|
adapter.clickListener.onLatestClick(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
source_latest.setOnClickListener {
|
pin.setOnClickListener {
|
||||||
adapter.latestClickListener.onLatestClick(bindingAdapterPosition)
|
adapter.clickListener.onPinClick(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (!showButtons) {
|
if (!showButtons) {
|
||||||
source_browse.gone()
|
source_latest.isVisible = false
|
||||||
source_latest.gone()
|
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@@ -59,11 +61,13 @@ class SourceHolder(view: View, override val adapter: SourceAdapter /* SY --> */,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
source_browse.setText(R.string.browse)
|
source_latest.isVisible = source.supportsLatest/* SY --> */ && showButtons /* SY <-- */
|
||||||
if (source.supportsLatest /* SY --> */ && showButtons /* SY <-- */) {
|
|
||||||
source_latest.visible()
|
pin.isVisible = showButtons
|
||||||
|
if (item.isPinned) {
|
||||||
|
pin.setVectorCompat(R.drawable.ic_push_pin_filled_24dp, view.context.getResourceColor(R.attr.colorAccent))
|
||||||
} else {
|
} else {
|
||||||
source_latest.gone()
|
pin.setVectorCompat(R.drawable.ic_push_pin_24dp, view.context.getResourceColor(android.R.attr.textColorHint))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
* @param source Instance of [CatalogueSource] containing source information.
|
* @param source Instance of [CatalogueSource] containing source information.
|
||||||
* @param header The header for this item.
|
* @param header The header for this item.
|
||||||
*/
|
*/
|
||||||
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null /* SY --> */, val showButtons: Boolean /* SY <-- */) :
|
data class SourceItem(
|
||||||
|
val source: CatalogueSource,
|
||||||
|
val header: LangItem? = null,
|
||||||
|
val isPinned: Boolean = false,
|
||||||
|
// SY -->
|
||||||
|
val showButtons: Boolean
|
||||||
|
// SY <--
|
||||||
|
) :
|
||||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,4 +49,15 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null
|
|||||||
) {
|
) {
|
||||||
holder.bind(this)
|
holder.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other is SourceItem) {
|
||||||
|
return source.id == other.source.id && getHeader()?.code == other.getHeader()?.code
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return source.id.hashCode() + (getHeader()?.code?.hashCode() ?: 0).toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,9 @@ class SourcePresenter(
|
|||||||
var sourceItems = byLang.flatMap {
|
var sourceItems = byLang.flatMap {
|
||||||
val langItem = LangItem(it.key)
|
val langItem = LangItem(it.key)
|
||||||
it.value.map { source ->
|
it.value.map { source ->
|
||||||
if (source.id.toString() in pinnedSourceIds) {
|
val isPinned = source.id.toString() in pinnedSourceIds
|
||||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), controllerMode == SourceController.Mode.CATALOGUE))
|
if (isPinned) {
|
||||||
|
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned, controllerMode == SourceController.Mode.CATALOGUE))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -106,6 +107,7 @@ class SourcePresenter(
|
|||||||
SourceItem(
|
SourceItem(
|
||||||
source,
|
source,
|
||||||
LangItem("custom|" + SourceAndCategory.second),
|
LangItem("custom|" + SourceAndCategory.second),
|
||||||
|
isPinned,
|
||||||
controllerMode == SourceController.Mode.CATALOGUE
|
controllerMode == SourceController.Mode.CATALOGUE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -115,7 +117,7 @@ class SourcePresenter(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
SourceItem(source, langItem, controllerMode == SourceController.Mode.CATALOGUE)
|
SourceItem(source, langItem, isPinned, controllerMode == SourceController.Mode.CATALOGUE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +149,10 @@ class SourcePresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLastUsedSource(sourceId: Long) {
|
private fun updateLastUsedSource(sourceId: Long) {
|
||||||
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let { SourceItem(it, showButtons = controllerMode == SourceController.Mode.CATALOGUE) }
|
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let {
|
||||||
|
val isPinned = it.id.toString() in preferences.pinnedSources().get()
|
||||||
|
SourceItem(it, null, isPinned, controllerMode == SourceController.Mode.CATALOGUE)
|
||||||
|
}
|
||||||
source?.let { view?.setLastUsedSource(it) }
|
source?.let { view?.setLastUsedSource(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-56
@@ -10,6 +10,7 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -18,6 +19,7 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import com.afollestad.materialdialogs.input.input
|
import com.afollestad.materialdialogs.input.input
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@@ -34,27 +36,25 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
|||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
|
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet.FilterNavigationView.Companion.MAX_SAVED_SEARCHES
|
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet.FilterNavigationView.Companion.MAX_SAVED_SEARCHES
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.inflate
|
import eu.kanade.tachiyomi.util.view.inflate
|
||||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import eu.kanade.tachiyomi.widget.EmptyView
|
import eu.kanade.tachiyomi.widget.EmptyView
|
||||||
import exh.EXHSavedSearch
|
import exh.EXHSavedSearch
|
||||||
|
import exh.isEhBasedSource
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@@ -64,7 +64,6 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +71,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
open class BrowseSourceController(bundle: Bundle) :
|
open class BrowseSourceController(bundle: Bundle) :
|
||||||
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||||
|
FabController,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
FlexibleAdapter.EndlessScrollListener,
|
FlexibleAdapter.EndlessScrollListener,
|
||||||
@@ -113,6 +113,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*/
|
*/
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
private var actionFab: ExtendedFloatingActionButton? = null
|
||||||
|
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snackbar containing an error message when a request fails.
|
* Snackbar containing an error message when a request fails.
|
||||||
*/
|
*/
|
||||||
@@ -146,7 +149,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
// SY -->
|
// SY -->
|
||||||
return when (mode) {
|
return when (mode) {
|
||||||
Mode.CATALOGUE -> presenter.source.name
|
Mode.CATALOGUE -> presenter.source.name
|
||||||
Mode.RECOMMENDS -> recommendsConfig!!.manga.title
|
Mode.RECOMMENDS -> recommendsConfig!!.title
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@@ -156,8 +159,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
return BrowseSourcePresenter(
|
return BrowseSourcePresenter(
|
||||||
args.getLong(SOURCE_ID_KEY),
|
args.getLong(SOURCE_ID_KEY),
|
||||||
args.getString(SEARCH_QUERY_KEY),
|
args.getString(SEARCH_QUERY_KEY),
|
||||||
searchManga = if (mode == Mode.RECOMMENDS) recommendsConfig?.manga else null,
|
recommendsMangaId = if (mode == Mode.RECOMMENDS) recommendsConfig?.mangaId else null
|
||||||
recommends = (mode == Mode.RECOMMENDS)
|
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@@ -177,7 +179,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
adapter = FlexibleAdapter(null, this)
|
adapter = FlexibleAdapter(null, this)
|
||||||
setupRecycler(view)
|
setupRecycler(view)
|
||||||
|
|
||||||
binding.progress.visible()
|
binding.progress.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun initFilterSheet() {
|
open fun initFilterSheet() {
|
||||||
@@ -189,7 +191,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
if (presenter.sourceFilters.isEmpty()) {
|
if (presenter.sourceFilters.isEmpty()) {
|
||||||
// SY -->
|
// SY -->
|
||||||
binding.fabFilter.text = activity!!.getString(R.string.eh_saved_searches)
|
actionFab?.text = activity!!.getString(R.string.saved_searches)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,8 +216,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
onSaveClicked = {
|
onSaveClicked = {
|
||||||
filterSheet?.context?.let {
|
filterSheet?.context?.let {
|
||||||
MaterialDialog(it)
|
MaterialDialog(it)
|
||||||
.title(text = "Save current search query?")
|
.title(R.string.save_search)
|
||||||
.input("My search name", hintRes = null) { _, searchName ->
|
.input(hintRes = R.string.save_search_hint) { _, searchName ->
|
||||||
val oldSavedSearches = presenter.loadSearches()
|
val oldSavedSearches = presenter.loadSearches()
|
||||||
if (searchName.isNotBlank() &&
|
if (searchName.isNotBlank() &&
|
||||||
oldSavedSearches.size < MAX_SAVED_SEARCHES
|
oldSavedSearches.size < MAX_SAVED_SEARCHES
|
||||||
@@ -244,8 +246,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
if (search == null) {
|
if (search == null) {
|
||||||
filterSheet?.context?.let {
|
filterSheet?.context?.let {
|
||||||
MaterialDialog(it)
|
MaterialDialog(it)
|
||||||
.title(text = "Failed to load saved searches!")
|
.title(R.string.save_search_failed_to_load)
|
||||||
.message(text = "An error occurred while loading your saved searches.")
|
.message(R.string.save_search_failed_to_load_message)
|
||||||
.cancelable(true)
|
.cancelable(true)
|
||||||
.cancelOnTouchOutside(true)
|
.cancelOnTouchOutside(true)
|
||||||
.show()
|
.show()
|
||||||
@@ -271,8 +273,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
if (search == null || search.name != name) {
|
if (search == null || search.name != name) {
|
||||||
filterSheet?.context?.let {
|
filterSheet?.context?.let {
|
||||||
MaterialDialog(it)
|
MaterialDialog(it)
|
||||||
.title(text = "Failed to delete saved search!")
|
.title(R.string.save_search_failed_to_delete)
|
||||||
.message(text = "An error occurred while deleting the search.")
|
.message(R.string.save_search_failed_to_delete_message)
|
||||||
.cancelable(true)
|
.cancelable(true)
|
||||||
.cancelOnTouchOutside(true)
|
.cancelOnTouchOutside(true)
|
||||||
.show()
|
.show()
|
||||||
@@ -282,10 +284,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
filterSheet?.context?.let {
|
filterSheet?.context?.let {
|
||||||
MaterialDialog(it)
|
MaterialDialog(it)
|
||||||
.title(text = "Delete saved search query?")
|
.title(R.string.save_search_delete)
|
||||||
.message(text = "Are you sure you wish to delete your saved search query: '${search.name}'?")
|
.message(text = it.getString(R.string.save_search_delete_message, search.name))
|
||||||
.positiveButton(R.string.action_cancel)
|
.positiveButton(R.string.action_cancel)
|
||||||
.negativeButton(text = "Confirm") {
|
.negativeButton(android.R.string.yes) {
|
||||||
val newSearches = savedSearches.filterIndexed { index, _ ->
|
val newSearches = savedSearches.filterIndexed { index, _ ->
|
||||||
index != indexToDelete
|
index != indexToDelete
|
||||||
}
|
}
|
||||||
@@ -299,17 +301,30 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
// EXH <--
|
// EXH <--
|
||||||
)
|
)
|
||||||
|
|
||||||
filterSheet?.setFilters(presenter.filterItems)
|
filterSheet?.setFilters(presenter.filterItems)
|
||||||
|
|
||||||
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
|
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
|
||||||
filterSheet?.setOnShowListener { binding.fabFilter.gone() }
|
filterSheet?.setOnShowListener { actionFab?.isVisible = false }
|
||||||
filterSheet?.setOnDismissListener { binding.fabFilter.visible() }
|
filterSheet?.setOnDismissListener { actionFab?.isVisible = true }
|
||||||
|
|
||||||
binding.fabFilter.setOnClickListener { filterSheet?.show() }
|
actionFab?.setOnClickListener { filterSheet?.show() }
|
||||||
|
|
||||||
binding.fabFilter.offsetAppbarHeight(activity!!)
|
actionFab?.isVisible = true
|
||||||
binding.fabFilter.visible()
|
}
|
||||||
|
|
||||||
|
override fun configureFab(fab: ExtendedFloatingActionButton) {
|
||||||
|
actionFab = fab
|
||||||
|
|
||||||
|
// Controlled by initFilterSheet()
|
||||||
|
fab.isVisible = false
|
||||||
|
|
||||||
|
fab.setText(R.string.action_filter)
|
||||||
|
fab.setIconResource(R.drawable.ic_filter_list_24dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||||
|
actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
|
||||||
|
actionFab = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
@@ -333,7 +348,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
binding.catalogueView.removeView(oldRecycler)
|
binding.catalogueView.removeView(oldRecycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST) {
|
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST /* SY --> */ || (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) /* SY <-- */) {
|
||||||
RecyclerView(view.context).apply {
|
RecyclerView(view.context).apply {
|
||||||
id = R.id.recycler
|
id = R.id.recycler
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
@@ -369,7 +384,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
)
|
)
|
||||||
recycler.clipToPadding = false
|
recycler.clipToPadding = false
|
||||||
|
|
||||||
binding.fabFilter.shrinkOnScroll(recycler)
|
actionFab?.shrinkOnScroll(recycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
recycler.setHasFixedSize(true)
|
recycler.setHasFixedSize(true)
|
||||||
@@ -386,12 +401,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.source_browse, menu)
|
inflater.inflate(R.menu.source_browse, menu)
|
||||||
|
|
||||||
// SY -->
|
|
||||||
if (mode == Mode.RECOMMENDS) {
|
|
||||||
menu.findItem(R.id.action_search).isVisible = false
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
// Initialize search menu
|
// Initialize search 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
|
||||||
@@ -424,6 +433,15 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
DisplayMode.LIST -> R.id.action_list
|
DisplayMode.LIST -> R.id.action_list
|
||||||
}
|
}
|
||||||
menu.findItem(displayItem).isChecked = true
|
menu.findItem(displayItem).isChecked = true
|
||||||
|
// SY -->
|
||||||
|
if (mode == Mode.RECOMMENDS) {
|
||||||
|
menu.findItem(R.id.action_search).isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) {
|
||||||
|
menu.findItem(R.id.action_display_mode).isVisible = false
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
@@ -624,8 +642,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
presenter.refreshDisplayMode()
|
presenter.refreshDisplayMode()
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
setupRecycler(view)
|
setupRecycler(view)
|
||||||
if (mode == DisplayMode.LIST || !view.context.connectivityManager.isActiveNetworkMetered) {
|
|
||||||
// Initialize mangas if going to grid view or if over wifi when going to list view
|
// Initialize mangas if not on a metered connection
|
||||||
|
if (!view.context.connectivityManager.isActiveNetworkMetered) {
|
||||||
val mangas = (0 until adapter.itemCount).mapNotNull {
|
val mangas = (0 until adapter.itemCount).mapNotNull {
|
||||||
(adapter.getItem(it) as? SourceItem)?.manga
|
(adapter.getItem(it) as? SourceItem)?.manga
|
||||||
}
|
}
|
||||||
@@ -670,7 +689,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*/
|
*/
|
||||||
private fun showProgressBar() {
|
private fun showProgressBar() {
|
||||||
binding.emptyView.hide()
|
binding.emptyView.hide()
|
||||||
binding.progress.visible()
|
binding.progress.isVisible = true
|
||||||
snack?.dismiss()
|
snack?.dismiss()
|
||||||
snack = null
|
snack = null
|
||||||
}
|
}
|
||||||
@@ -680,7 +699,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*/
|
*/
|
||||||
private fun hideProgressBar() {
|
private fun hideProgressBar() {
|
||||||
binding.emptyView.hide()
|
binding.emptyView.hide()
|
||||||
binding.progress.gone()
|
binding.progress.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -694,25 +713,15 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
// SY -->
|
// SY -->
|
||||||
when (mode) {
|
when (mode) {
|
||||||
Mode.CATALOGUE -> {
|
Mode.CATALOGUE -> {
|
||||||
if (preferences.eh_useNewMangaInterface().get()) {
|
router.pushController(
|
||||||
router.pushController(
|
MangaController(
|
||||||
MangaAllInOneController(
|
item.manga,
|
||||||
item.manga,
|
true,
|
||||||
true,
|
args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA)
|
||||||
args.getParcelable(SMART_SEARCH_CONFIG_KEY)
|
).withFadeTransaction()
|
||||||
).withFadeTransaction()
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
router.pushController(
|
|
||||||
MangaController(
|
|
||||||
item.manga,
|
|
||||||
true,
|
|
||||||
args.getParcelable(SMART_SEARCH_CONFIG_KEY)
|
|
||||||
).withFadeTransaction()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Mode.RECOMMENDS -> openSmartSearch(item.manga.title)
|
Mode.RECOMMENDS -> openSmartSearch(item.manga.originalTitle)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
return false
|
return false
|
||||||
@@ -821,7 +830,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RecommendsConfig(val manga: Manga) : Parcelable
|
data class RecommendsConfig(val title: String, val mangaId: Long?) : Parcelable
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
CATALOGUE,
|
CATALOGUE,
|
||||||
|
|||||||
+29
-11
@@ -38,8 +38,9 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
|
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import exh.EXHSavedSearch
|
import exh.EXHSavedSearch
|
||||||
|
import exh.isEhBasedSource
|
||||||
import java.lang.RuntimeException
|
import java.lang.RuntimeException
|
||||||
import kotlinx.coroutines.flow.subscribe
|
import java.util.Date
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@@ -56,14 +57,13 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
|||||||
open class BrowseSourcePresenter(
|
open class BrowseSourcePresenter(
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
private val searchQuery: String? = null,
|
private val searchQuery: String? = null,
|
||||||
private val searchManga: Manga? = null,
|
// SY -->
|
||||||
|
private val recommendsMangaId: Long? = null,
|
||||||
|
// SY <--
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
private val prefs: PreferencesHelper = Injekt.get(),
|
private val prefs: PreferencesHelper = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get()
|
||||||
// SY -->
|
|
||||||
private val recommends: Boolean = false
|
|
||||||
// SY <--
|
|
||||||
) : BasePresenter<BrowseSourceController>() {
|
) : BasePresenter<BrowseSourceController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +74,7 @@ open class BrowseSourcePresenter(
|
|||||||
/**
|
/**
|
||||||
* Query from the view.
|
* Query from the view.
|
||||||
*/
|
*/
|
||||||
var query = /* SY --> */ if (recommends) "" else /* SY <-- */ searchQuery ?: ""
|
var query = searchQuery ?: ""
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,6 +113,10 @@ open class BrowseSourcePresenter(
|
|||||||
*/
|
*/
|
||||||
private var pageSubscription: Subscription? = null
|
private var pageSubscription: Subscription? = null
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private var manga: Manga? = null
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to initialize manga details.
|
* Subscription to initialize manga details.
|
||||||
*/
|
*/
|
||||||
@@ -129,6 +133,10 @@ open class BrowseSourcePresenter(
|
|||||||
query = savedState.getString(::query.name, "")
|
query = savedState.getString(::query.name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recommendsMangaId != null) {
|
||||||
|
manga = db.getManga(recommendsMangaId).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
restartPager()
|
restartPager()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,8 +159,8 @@ open class BrowseSourcePresenter(
|
|||||||
|
|
||||||
// Create a new pager.
|
// Create a new pager.
|
||||||
// SY -->
|
// SY -->
|
||||||
pager = if (recommends && searchManga != null) RecommendsPager(
|
pager = if (recommendsMangaId != null && manga != null) RecommendsPager(
|
||||||
searchManga
|
manga ?: throw Exception("Could not get Manga")
|
||||||
) else createPager(query, filters)
|
) else createPager(query, filters)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -164,9 +172,13 @@ open class BrowseSourcePresenter(
|
|||||||
pagerSubscription?.let { remove(it) }
|
pagerSubscription?.let { remove(it) }
|
||||||
pagerSubscription = pager.results()
|
pagerSubscription = pager.results()
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.map { pair -> pair.first to pair.second.map { networkToLocalManga(it, sourceId) } }
|
// SY -->
|
||||||
|
.map { triple -> Triple(triple.first, triple.second.map { networkToLocalManga(it, sourceId) }, triple.third) }
|
||||||
|
// SY <--
|
||||||
.doOnNext { initializeMangas(it.second) }
|
.doOnNext { initializeMangas(it.second) }
|
||||||
.map { pair -> pair.first to pair.second.map { SourceItem(it, sourceDisplayMode) } }
|
// SY -->
|
||||||
|
.map { triple -> triple.first to triple.second.mapIndexed { index, manga -> SourceItem(manga, sourceDisplayMode, if (prefs.enhancedEHentaiView().get() && source.isEhBasedSource()) triple.third?.getOrNull(index) else null) } }
|
||||||
|
// SY <--
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeReplay(
|
.subscribeReplay(
|
||||||
{ view, (page, mangas) ->
|
{ view, (page, mangas) ->
|
||||||
@@ -279,9 +291,15 @@ open class BrowseSourcePresenter(
|
|||||||
*/
|
*/
|
||||||
fun changeMangaFavorite(manga: Manga) {
|
fun changeMangaFavorite(manga: Manga) {
|
||||||
manga.favorite = !manga.favorite
|
manga.favorite = !manga.favorite
|
||||||
|
manga.date_added = when (manga.favorite) {
|
||||||
|
true -> Date().time
|
||||||
|
false -> 0
|
||||||
|
}
|
||||||
|
|
||||||
if (!manga.favorite) {
|
if (!manga.favorite) {
|
||||||
manga.removeCovers(coverCache)
|
manga.removeCovers(coverCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
|||||||
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,9 +15,9 @@ abstract class Pager(var currentPage: Int = 1) {
|
|||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
private set
|
private set
|
||||||
|
|
||||||
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
|
protected val results: PublishRelay< /* SY --> */ Triple /* SY <-- */ <Int, List<SManga> /* SY --> */, List<RaisedSearchMetadata>? /* SY <-- */ >> = PublishRelay.create()
|
||||||
|
|
||||||
fun results(): Observable<Pair<Int, List<SManga>>> {
|
fun results(): Observable< /* SY --> */ Triple /* SY <-- */ <Int, List<SManga> /* SY --> */, List<RaisedSearchMetadata>?> /* SY <-- */> {
|
||||||
return results.asObservable()
|
return results.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +27,11 @@ abstract class Pager(var currentPage: Int = 1) {
|
|||||||
val page = currentPage
|
val page = currentPage
|
||||||
currentPage++
|
currentPage++
|
||||||
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
|
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
|
||||||
results.call(Pair(page, mangasPage.mangas))
|
// SY -->
|
||||||
|
val mangasMetadata = if (mangasPage is MetadataMangasPage) {
|
||||||
|
mangasPage.mangasMetadata
|
||||||
|
} else null
|
||||||
|
// SY <--
|
||||||
|
results.call( /* SY <-- */ Triple /* SY <-- */ (page, mangasPage.mangas /* SY --> */, mangasMetadata /* SY <-- */))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
|
|
||||||
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
||||||
|
|
||||||
@@ -25,17 +24,17 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
|
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
|
||||||
holder.progressBar.gone()
|
holder.progressBar.isVisible = false
|
||||||
holder.progressMessage.gone()
|
holder.progressMessage.isVisible = false
|
||||||
|
|
||||||
if (!adapter.isEndlessScrollEnabled) {
|
if (!adapter.isEndlessScrollEnabled) {
|
||||||
loadMore = false
|
loadMore = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadMore) {
|
if (loadMore) {
|
||||||
holder.progressBar.visible()
|
holder.progressBar.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
holder.progressMessage.visible()
|
holder.progressMessage.isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ open class RecommendsPager(
|
|||||||
|
|
||||||
private fun getRecs(api: API) {
|
private fun getRecs(api: API) {
|
||||||
Timber.tag("RECOMMENDATIONS").d("USING > %s", api.toString())
|
Timber.tag("RECOMMENDATIONS").d("USING > %s", api.toString())
|
||||||
apiList[api]?.getRecsBySearch(manga.title) { recs, error ->
|
apiList[api]?.getRecsBySearch(manga.originalTitle) { recs, error ->
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -3,11 +3,10 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import eu.kanade.tachiyomi.databinding.SourceFilterSheetSavedSearchesBinding
|
import eu.kanade.tachiyomi.databinding.SourceFilterSheetSavedSearchesBinding
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
|
|
||||||
class SavedSearchesAdapter(var chips: List<Chip> = emptyList()) :
|
class SavedSearchesAdapter(var chips: List<Chip> = emptyList()) :
|
||||||
RecyclerView.Adapter<SavedSearchesAdapter.SavedSearchesViewHolder>() {
|
RecyclerView.Adapter<SavedSearchesAdapter.SavedSearchesViewHolder>() {
|
||||||
@@ -29,9 +28,9 @@ class SavedSearchesAdapter(var chips: List<Chip> = emptyList()) :
|
|||||||
fun bind(chips: List<Chip> = emptyList()) {
|
fun bind(chips: List<Chip> = emptyList()) {
|
||||||
binding.savedSearches.removeAllViews()
|
binding.savedSearches.removeAllViews()
|
||||||
if (chips.isEmpty()) {
|
if (chips.isEmpty()) {
|
||||||
binding.savedSearchesTitle.gone()
|
binding.savedSearchesTitle.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
binding.savedSearchesTitle.visible()
|
binding.savedSearchesTitle.isVisible = true
|
||||||
chips.forEach {
|
chips.forEach {
|
||||||
binding.savedSearches.addView(it)
|
binding.savedSearches.addView(it)
|
||||||
}
|
}
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.View
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
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.util.system.getResourceColor
|
||||||
|
import exh.metadata.EX_DATE_FORMAT
|
||||||
|
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||||
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
|
import exh.util.SourceTagsUtil
|
||||||
|
import exh.util.SourceTagsUtil.Companion.getLocaleSourceUtil
|
||||||
|
import java.util.Date
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.date_posted
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.genre
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.language
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.rating_bar
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.thumbnail
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.title
|
||||||
|
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.uploader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
|
* All the elements from the layout file "item_catalogue_list" are available in this class.
|
||||||
|
*
|
||||||
|
* @param view the inflated view for this holder.
|
||||||
|
* @param adapter the adapter handling this holder.
|
||||||
|
* @constructor creates a new catalogue holder.
|
||||||
|
*/
|
||||||
|
class SourceEnhancedEHentaiListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
||||||
|
SourceHolder(view, adapter) {
|
||||||
|
|
||||||
|
private val favoriteColor = view.context.getResourceColor(R.attr.colorOnSurface, 0.38f)
|
||||||
|
private val unfavoriteColor = view.context.getResourceColor(R.attr.colorOnSurface)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||||
|
* holder with the given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to bind.
|
||||||
|
*/
|
||||||
|
override fun onSetValues(manga: Manga) {
|
||||||
|
title.text = manga.title
|
||||||
|
title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
|
||||||
|
|
||||||
|
// Set alpha of thumbnail.
|
||||||
|
thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||||
|
|
||||||
|
setImage(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
|
||||||
|
if (metadata !is EHentaiSearchMetadata) return
|
||||||
|
|
||||||
|
if (metadata.uploader != null) {
|
||||||
|
uploader.text = metadata.uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
val pair = when (metadata.genre) {
|
||||||
|
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
|
||||||
|
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
|
||||||
|
"artistcg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
|
||||||
|
"gamecg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
|
||||||
|
"western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western)
|
||||||
|
"non-h" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h)
|
||||||
|
"imageset" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set)
|
||||||
|
"cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay)
|
||||||
|
"asianporn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn)
|
||||||
|
"misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc)
|
||||||
|
else -> Pair("", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pair.first.isNotBlank()) {
|
||||||
|
genre.setBackgroundColor(Color.parseColor(pair.first))
|
||||||
|
genre.text = view.context.getString(pair.second)
|
||||||
|
} else genre.text = metadata.genre
|
||||||
|
|
||||||
|
metadata.datePosted?.let { date_posted.text = EX_DATE_FORMAT.format(Date(it)) }
|
||||||
|
|
||||||
|
metadata.averageRating?.let { rating_bar.rating = it.toFloat() }
|
||||||
|
|
||||||
|
val locale = getLocaleSourceUtil(metadata.tags.firstOrNull { it.namespace == "language" }?.name)
|
||||||
|
val pageCount = metadata.length
|
||||||
|
|
||||||
|
language.text = if (locale != null && pageCount != null) {
|
||||||
|
view.resources.getQuantityString(R.plurals.browse_language_and_pages, pageCount, pageCount, locale.toLanguageTag().toUpperCase())
|
||||||
|
} else if (pageCount != null) {
|
||||||
|
view.resources.getQuantityString(R.plurals.num_pages, pageCount, pageCount)
|
||||||
|
} else locale?.toLanguageTag()?.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setImage(manga: Manga) {
|
||||||
|
GlideApp.with(view.context).clear(thumbnail)
|
||||||
|
|
||||||
|
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||||
|
val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius)
|
||||||
|
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
|
||||||
|
GlideApp.with(view.context)
|
||||||
|
.load(manga.toMangaThumbnail())
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||||
|
.apply(requestOptions)
|
||||||
|
.dontAnimate()
|
||||||
|
.placeholder(android.R.color.transparent)
|
||||||
|
.into(thumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user