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
|
||||
|
||||
# Bugs
|
||||
* Include version (Setting > About > Version)
|
||||
* Include version (More > About > Version)
|
||||
* 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 screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ Features of TachiyomiSY include:
|
||||
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
||||
* Comfortable grid view
|
||||
* 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
|
||||
* 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
|
||||
* * Hitomi.la
|
||||
* * 8Muses
|
||||
* * HBrowse
|
||||
* * Perv Eden
|
||||
* Additional features for some extensions, features include custom description, opening in app, batch add to library:
|
||||
* * Puruin
|
||||
* * Tsumino
|
||||
* * HentaiCafe (Foolside)
|
||||
* * HBrowse
|
||||
* Saving searches
|
||||
* Autoscroll
|
||||
* 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>
|
||||
|
||||
* Include version (Setting > About > Version)
|
||||
* Include version (More > About > Version)
|
||||
* 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
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
|
||||
+26
-19
@@ -36,15 +36,14 @@ ext {
|
||||
android {
|
||||
compileSdkVersion AndroidConfig.compileSdk
|
||||
buildToolsVersion AndroidConfig.buildTools
|
||||
publishNonDefault true
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi.sy"
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 2
|
||||
versionName "1.0.0"
|
||||
versionCode 4
|
||||
versionName "1.1.1"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@@ -129,7 +128,7 @@ android {
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,17 +138,13 @@ androidExtensions {
|
||||
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// AndroidX libraries
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
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.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.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
@@ -168,9 +163,9 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
// 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
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
@@ -204,6 +199,7 @@ dependencies {
|
||||
// Disk
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// HTML parser
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
@@ -221,7 +217,7 @@ dependencies {
|
||||
implementation 'io.requery:sqlite-android:3.31.0'
|
||||
|
||||
// Preferences
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
@@ -237,12 +233,14 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
// Crash reports
|
||||
final acra_version = '5.5.0'
|
||||
implementation "ch.acra:acra-http:$acra_version"
|
||||
//final acra_version = '5.5.0'
|
||||
//implementation "ch.acra:acra-http:$acra_version"
|
||||
|
||||
// Sort
|
||||
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'
|
||||
|
||||
// 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-appcompat:$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-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-android:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$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)
|
||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||
|
||||
@@ -338,6 +342,9 @@ dependencies {
|
||||
// Humanize (EH)
|
||||
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'
|
||||
|
||||
final def markwon_version = '4.1.0'
|
||||
@@ -380,7 +387,7 @@ task copyResources(type: Copy) {
|
||||
|
||||
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'
|
||||
// Firebase (EH)
|
||||
apply plugin: 'io.fabric'
|
||||
|
||||
@@ -39,11 +39,6 @@
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.main.ForceCloseActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:launchMode="singleTask"
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
@@ -29,11 +28,8 @@ import com.ms_square.debugoverlay.DebugOverlay
|
||||
import com.ms_square.debugoverlay.modules.FpsModule
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
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.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.debug.DebugToggles
|
||||
import exh.log.CrashlyticsPrinter
|
||||
import exh.log.EHDebugModeOverlay
|
||||
@@ -63,11 +59,14 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
workaroundAndroid7BrokenSSL()
|
||||
|
||||
// Enforce WebView availability
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
ForceCloseActivity.closeApp(this)
|
||||
}
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// SoLoader.init(this, false)
|
||||
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(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
|
||||
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.database.DatabaseHelper
|
||||
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.track.TrackManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
@@ -42,6 +43,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { DownloadManager(app) }
|
||||
|
||||
addSingletonFactory { CustomMangaManager(app) }
|
||||
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
@@ -63,5 +66,9 @@ class AppModule(val app: Application) : InjektModule {
|
||||
GlobalScope.launch { get<DatabaseHelper>() }
|
||||
|
||||
GlobalScope.launch { get<DownloadManager>() }
|
||||
|
||||
// SY -->
|
||||
GlobalScope.launch { get<CustomMangaManager>() }
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ object Migrations {
|
||||
}
|
||||
if (oldVersion < 44) {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
@Suppress("DEPRECATION")
|
||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
@@ -106,7 +107,7 @@ class BackupCreateService : Service() {
|
||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||
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)
|
||||
notifier.showBackupComplete(unifile)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
@@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val backupManager = BackupManager(context)
|
||||
val uri = Uri.parse(preferences.backupsDirectory().get())
|
||||
val uri = preferences.backupsDirectory().get().toUri()
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
return try {
|
||||
backupManager.createBackup(uri, flags, true)
|
||||
|
||||
@@ -359,7 +359,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.nameLower == dbCategory.nameLower) {
|
||||
if (category.name == dbCategory.name) {
|
||||
category.id = dbCategory.id
|
||||
found = true
|
||||
break
|
||||
@@ -387,7 +387,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||
for (backupCategoryStr in categories) {
|
||||
for (dbCategory in dbCategories) {
|
||||
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
||||
if (backupCategoryStr == dbCategory.name) {
|
||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ class BackupRestoreService : Service() {
|
||||
|
||||
// SY -->
|
||||
private val throttleManager = EHentaiThrottleManager()
|
||||
|
||||
private var skippedAmount = 0
|
||||
|
||||
private var totalAmount = 0
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
@@ -117,12 +121,6 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private var restoreAmount = 0
|
||||
|
||||
// SY -->
|
||||
private var skippedAmount = 0
|
||||
|
||||
private var totalAmount = 0
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Mapping of source ID to source name from backup data
|
||||
*/
|
||||
@@ -288,7 +286,7 @@ class BackupRestoreService : Service() {
|
||||
backupManager.restoreSavedSearches(savedSearchesJson)
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.eh_saved_searches))
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -320,13 +318,8 @@ class BackupRestoreService : Service() {
|
||||
if (source != null) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||
} else {
|
||||
val message = if (manga.source in sourceMapping) {
|
||||
getString(R.string.source_not_found_name, sourceMapping[manga.source])
|
||||
} else {
|
||||
getString(R.string.source_not_found)
|
||||
}
|
||||
|
||||
errors.add(Date() to "${manga.title} - $message")
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
|
||||
@@ -14,7 +14,9 @@ object MangaTypeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.url)
|
||||
value(it.title)
|
||||
// SY -->
|
||||
value(it.originalTitle)
|
||||
// SY <--
|
||||
value(it.source)
|
||||
value(it.viewer)
|
||||
value(it.chapter_flags)
|
||||
|
||||
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* 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) {
|
||||
@@ -66,6 +66,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
if (oldVersion < 2) {
|
||||
db.execSQL(MangaTable.addCoverLastModified)
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
db.execSQL(MangaTable.addDateAdded)
|
||||
db.execSQL(MangaTable.backfillDateAdded)
|
||||
}
|
||||
}
|
||||
|
||||
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_CHAPTER_FLAGS
|
||||
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_FAVORITE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||
@@ -47,15 +48,17 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_SOURCE, obj.source)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_ARTIST, obj.artist)
|
||||
put(COL_AUTHOR, obj.author)
|
||||
put(COL_DESCRIPTION, obj.description)
|
||||
put(COL_GENRE, obj.genre)
|
||||
put(COL_TITLE, obj.title)
|
||||
// SY -->
|
||||
put(COL_ARTIST, obj.originalArtist)
|
||||
put(COL_AUTHOR, obj.originalAuthor)
|
||||
put(COL_DESCRIPTION, obj.originalDescription)
|
||||
put(COL_GENRE, obj.originalGenre)
|
||||
put(COL_TITLE, obj.originalTitle)
|
||||
// SY <--
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||
put(COL_FAVORITE, obj.favorite)
|
||||
@@ -64,6 +67,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
put(COL_VIEWER, obj.viewer)
|
||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||
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))
|
||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||
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>
|
||||
// SY <--
|
||||
|
||||
val nameLower: String
|
||||
get() = name.toLowerCase()
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
|
||||
@@ -19,7 +19,6 @@ class CategoryImpl : Category {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val category = other as Category
|
||||
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val chapter = other as Chapter
|
||||
return url == chapter.url
|
||||
if (url != chapter.url) return false
|
||||
return id == chapter.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
return url.hashCode() + id.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Manga : SManga {
|
||||
|
||||
var last_update: Long
|
||||
|
||||
var date_added: Long
|
||||
|
||||
var viewer: Int
|
||||
|
||||
var chapter_flags: Int
|
||||
@@ -22,10 +24,6 @@ interface Manga : SManga {
|
||||
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 {
|
||||
return chapter_flags and SORT_MASK == SORT_DESC
|
||||
}
|
||||
@@ -34,6 +32,10 @@ interface Manga : SManga {
|
||||
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
|
||||
var displayMode: Int
|
||||
get() = chapter_flags and DISPLAY_MASK
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
@@ -9,17 +12,36 @@ open class MangaImpl : Manga {
|
||||
override lateinit var url: String
|
||||
|
||||
// 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 <--
|
||||
|
||||
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 thumbnail_url: String? = null
|
||||
@@ -28,6 +50,8 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var last_update: Long = 0
|
||||
|
||||
override var date_added: Long = 0
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var viewer: Int = 0
|
||||
@@ -36,16 +60,29 @@ open class MangaImpl : Manga {
|
||||
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val manga = other as Manga
|
||||
|
||||
return url == manga.url
|
||||
if (url != manga.url) return false
|
||||
return id == manga.id
|
||||
}
|
||||
|
||||
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.MangaFavoritePutResolver
|
||||
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.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||
@@ -84,6 +85,16 @@ interface MangaQueries : DbProvider {
|
||||
.build()
|
||||
)
|
||||
.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 <--
|
||||
|
||||
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}
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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_DATE_ADDED = "date_added"
|
||||
|
||||
const val COL_INITIALIZED = "initialized"
|
||||
|
||||
const val COL_VIEWER = "viewer"
|
||||
@@ -58,7 +60,8 @@ object MangaTable {
|
||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||
$COL_VIEWER 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
|
||||
@@ -70,4 +73,17 @@ object MangaTable {
|
||||
|
||||
val addCoverLastModified: String
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -59,7 +59,7 @@ class DownloadCache(
|
||||
*/
|
||||
private fun getDirectoryFromPreference(): UniFile {
|
||||
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) {
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return provider.getChapterDirName(chapter) in mangaDir.files
|
||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -122,7 +122,9 @@ class DownloadCache(
|
||||
* Renews the downloads cache.
|
||||
*/
|
||||
private fun renew() {
|
||||
val onlineSources = sourceManager.getOnlineSources()
|
||||
// SY -->
|
||||
val onlineSources = sourceManager.getVisibleOnlineSources()
|
||||
// SY <--
|
||||
|
||||
val sourceDirs = rootDir.dir.listFiles()
|
||||
.orEmpty()
|
||||
@@ -191,9 +193,10 @@ class DownloadCache(
|
||||
fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
val chapterDirName = provider.getChapterDirName(chapter)
|
||||
if (chapterDirName in mangaDir.files) {
|
||||
mangaDir.files -= chapterDirName
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,9 +211,10 @@ class DownloadCache(
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
chapters.forEach { chapter ->
|
||||
val chapterDirName = provider.getChapterDirName(chapter)
|
||||
if (chapterDirName in mangaDir.files) {
|
||||
mangaDir.files -= chapterDirName
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(private val context: Context) {
|
||||
class DownloadManager(/* SY private */ val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
@@ -251,16 +251,20 @@ class DownloadManager(private val context: Context) {
|
||||
* @param newChapter the target chapter with the new name.
|
||||
*/
|
||||
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 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) {
|
||||
cache.removeChapter(oldChapter, manga)
|
||||
cache.addChapter(newName, mangaDir, manga)
|
||||
} 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.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
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.
|
||||
*/
|
||||
@Volatile
|
||||
private var isDownloading = false
|
||||
|
||||
/**
|
||||
@@ -50,14 +62,14 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*
|
||||
* @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())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old actions if they exist.
|
||||
*/
|
||||
private fun clearActions() = with(notificationBuilder) {
|
||||
private fun NotificationCompat.Builder.clearActions() {
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
@@ -68,7 +80,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* those can only be dismissed by the user.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
fun onProgressChange(download: Download) {
|
||||
// Create notification
|
||||
with(notificationBuilder) {
|
||||
with(progressNotificationBuilder) {
|
||||
// Check if first call.
|
||||
if (!isDownloading) {
|
||||
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)
|
||||
}
|
||||
|
||||
// Displays the progress bar on notification
|
||||
notificationBuilder.show()
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification when download is paused.
|
||||
*/
|
||||
fun onDownloadPaused() {
|
||||
with(notificationBuilder) {
|
||||
fun onPaused() {
|
||||
with(progressNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.chapter_paused))
|
||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||
@@ -141,22 +151,45 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
context.getString(R.string.action_cancel_all),
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||
)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
|
||||
// Show notification.
|
||||
notificationBuilder.show()
|
||||
|
||||
// Reset initial values
|
||||
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.
|
||||
*
|
||||
* @param reason the text to show.
|
||||
*/
|
||||
fun onWarning(reason: String) {
|
||||
with(notificationBuilder) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(reason)
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
@@ -164,8 +197,9 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
|
||||
// Reset download information
|
||||
isDownloading = false
|
||||
@@ -180,7 +214,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*/
|
||||
fun onError(error: String? = null, chapter: String? = null) {
|
||||
// Create notification
|
||||
with(notificationBuilder) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(
|
||||
chapter
|
||||
?: context.getString(R.string.download_notifier_downloader_title)
|
||||
@@ -191,8 +225,9 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setAutoCancel(false)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
@@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
||||
lastEntry.copy(chapters = newChapters)
|
||||
} else {
|
||||
val existingEntry = prefs.getString(manga.id!!.toString(), null)
|
||||
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
||||
if (existingEntry != null) {
|
||||
// Existing entry found on preferences, decode json and add the new chapter
|
||||
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
||||
@@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
|
||||
|
||||
// Save current state
|
||||
val json = gson.toJson(newEntry)
|
||||
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
|
||||
preferences.edit {
|
||||
putString(newEntry.manga.id.toString(), json)
|
||||
}
|
||||
lastAddedEntry = newEntry
|
||||
}
|
||||
|
||||
@@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
|
||||
@Synchronized
|
||||
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
||||
val entries = decodeAll()
|
||||
prefs.edit().clear().apply()
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
lastAddedEntry = null
|
||||
|
||||
return entries.associate { entry ->
|
||||
@@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
* Decodes all the chapters from preferences.
|
||||
*/
|
||||
private fun decodeAll(): List<Entry> {
|
||||
return prefs.all.values.mapNotNull { rawEntry ->
|
||||
return preferences.all.values.mapNotNull { rawEntry ->
|
||||
try {
|
||||
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
||||
} catch (e: Exception) {
|
||||
@@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
|
||||
private data class ChapterEntry(
|
||||
val id: Long,
|
||||
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.
|
||||
*/
|
||||
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.url = url
|
||||
it.name = name
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
|
||||
* The root directory for downloads.
|
||||
*/
|
||||
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)
|
||||
dir
|
||||
}
|
||||
|
||||
init {
|
||||
preferences.downloadsDirectory().asFlow()
|
||||
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,9 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||
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> {
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo.State.CONNECTED
|
||||
import android.net.NetworkInfo.State.DISCONNECTED
|
||||
import android.os.Build
|
||||
@@ -82,7 +83,7 @@ class DownloadService : Service() {
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
runningRelay.call(true)
|
||||
subscriptions = CompositeSubscription()
|
||||
@@ -143,7 +144,7 @@ class DownloadService : Service() {
|
||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||
when (connectivity.state) {
|
||||
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))
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
@@ -175,19 +176,19 @@ class DownloadService : Service() {
|
||||
/**
|
||||
* Releases the wake lock if it's held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
private fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
if (isHeld) release()
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires the wake lock if it's not held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
private fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
if (!isHeld) acquire()
|
||||
}
|
||||
|
||||
private fun getPlaceholderNotification(): Notification {
|
||||
return notification(Notifications.CHANNEL_DOWNLOADER) {
|
||||
return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -42,9 +43,9 @@ class DownloadStore(
|
||||
* @param downloads the list of downloads to add.
|
||||
*/
|
||||
fun addAll(downloads: List<Download>) {
|
||||
val editor = preferences.edit()
|
||||
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
|
||||
editor.apply()
|
||||
preferences.edit {
|
||||
downloads.forEach { putString(getKey(it), serialize(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,14 +54,18 @@ class DownloadStore(
|
||||
* @param download the download to remove.
|
||||
*/
|
||||
fun remove(download: Download) {
|
||||
preferences.edit().remove(getKey(download)).apply()
|
||||
preferences.edit {
|
||||
remove(getKey(download))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the downloads from the store.
|
||||
*/
|
||||
fun clear() {
|
||||
preferences.edit().clear().apply()
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,9 +137,9 @@ class Downloader(
|
||||
} else {
|
||||
if (notifier.paused) {
|
||||
notifier.paused = false
|
||||
notifier.onDownloadPaused()
|
||||
notifier.onPaused()
|
||||
} else {
|
||||
notifier.dismiss()
|
||||
notifier.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,13 +231,9 @@ class Downloader(
|
||||
val wasEmpty = queue.isEmpty()
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
val mangaDir = provider.findMangaDir(manga, source)
|
||||
|
||||
chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// 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.
|
||||
.sortedByDescending { it.source_order }
|
||||
}
|
||||
@@ -272,6 +268,13 @@ class Downloader(
|
||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
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 pageListObservable = if (download.pages == null) {
|
||||
@@ -489,5 +492,8 @@ class Downloader(
|
||||
|
||||
companion object {
|
||||
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.
|
||||
*/
|
||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER = -201
|
||||
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -50,7 +54,7 @@ object Notifications {
|
||||
/**
|
||||
* 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 ID_BACKUP_PROGRESS = -501
|
||||
const val ID_RESTORE_PROGRESS = -503
|
||||
@@ -59,6 +63,7 @@ object Notifications {
|
||||
const val ID_RESTORE_COMPLETE = -504
|
||||
|
||||
private val deprecatedChannels = listOf(
|
||||
"downloader_channel",
|
||||
"backup_restore_complete_channel"
|
||||
)
|
||||
|
||||
@@ -70,10 +75,12 @@ object Notifications {
|
||||
fun createChannels(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
|
||||
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
|
||||
listOf(
|
||||
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(
|
||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
@@ -85,9 +92,24 @@ object Notifications {
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
||||
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
||||
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)
|
||||
},
|
||||
NotificationChannel(
|
||||
@@ -99,26 +121,23 @@ object Notifications {
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
),
|
||||
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
|
||||
).apply {
|
||||
group = GROUP_BACK_RESTORE
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
setShowBadge(false)
|
||||
},
|
||||
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
|
||||
).apply {
|
||||
group = GROUP_BACK_RESTORE
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(channels)
|
||||
).forEach(context.notificationManager::createNotificationChannel)
|
||||
|
||||
// Delete old notification channels
|
||||
deprecatedChannels.forEach {
|
||||
context.notificationManager.deleteNotificationChannel(it)
|
||||
}
|
||||
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ object PreferenceKeys {
|
||||
|
||||
const val readWithTapping = "reader_tap"
|
||||
|
||||
const val readWithTappingInverted = "reader_tapping_inverted"
|
||||
|
||||
const val readWithLongTap = "reader_long_tap"
|
||||
|
||||
const val readWithVolumeKeys = "reader_volume_keys"
|
||||
@@ -67,6 +69,8 @@ object PreferenceKeys {
|
||||
|
||||
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 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_use_new_manga_interface = "eh_use_new_manga_interface"
|
||||
|
||||
const val eh_use_auto_webtoon = "eh_use_auto_webtoon"
|
||||
|
||||
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 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,
|
||||
LIST,
|
||||
}
|
||||
|
||||
enum class TappingInvertMode {
|
||||
NONE,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
BOTH
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
@@ -27,27 +27,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||
.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)
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val flowPrefs = FlowSharedPreferences(prefs)
|
||||
|
||||
private val defaultDownloadsDir = Uri.fromFile(
|
||||
File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"downloads"
|
||||
)
|
||||
)
|
||||
private val defaultDownloadsDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"downloads"
|
||||
).toUri()
|
||||
|
||||
private val defaultBackupDir = Uri.fromFile(
|
||||
File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"backup"
|
||||
)
|
||||
)
|
||||
private val defaultBackupDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"backup"
|
||||
).toUri()
|
||||
|
||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||
|
||||
@@ -121,6 +125,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
||||
|
||||
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
|
||||
|
||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
||||
|
||||
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||
@@ -131,6 +137,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||
|
||||
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
||||
|
||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||
|
||||
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_useNewMangaInterface() = flowPrefs.getBoolean(Keys.eh_use_new_manga_interface, true)
|
||||
|
||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
||||
|
||||
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 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
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
@@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return baseMangaUrl + mediaId
|
||||
}
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "token")
|
||||
.build()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.google.gson.Gson
|
||||
@@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse(
|
||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
).buildUpon()
|
||||
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
@@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
return "$baseMangaUrl/$remoteId"
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
fun authUrl(): Uri =
|
||||
loginUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.track.TrackManager
|
||||
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 loginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
private fun loginUrl() = baseUrl.toUri().buildUpon()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
|
||||
private fun searchUrl(query: String): String {
|
||||
val col = "c[]"
|
||||
return Uri.parse(baseUrl).buildUpon()
|
||||
return baseUrl.toUri().buildUpon()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
@@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
private fun exportListUrl() = baseUrl.toUri().buildUpon()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
|
||||
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
|
||||
.appendPath(mediaId.toString())
|
||||
.appendPath("edit")
|
||||
.toString()
|
||||
|
||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
|
||||
.appendPath("add.json")
|
||||
.toString()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.jsonObject
|
||||
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 search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
@@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
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("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
@@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val requestMangas = Request.Builder()
|
||||
@@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
loginUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.model.Extension
|
||||
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.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
@@ -88,7 +88,6 @@ class ExtensionManager(
|
||||
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
|
||||
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_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)
|
||||
else -> null
|
||||
}
|
||||
@@ -319,8 +318,7 @@ class ExtensionManager(
|
||||
if (signature !in untrustedSignatures) return
|
||||
|
||||
ExtensionLoader.trustedSignatures += signature
|
||||
val preference = preferences.trustedSignatures()
|
||||
preference.set(preference.get() + signature)
|
||||
preferences.trustedSignatures() += signature
|
||||
|
||||
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
||||
untrustedExtensions -= nowTrustedExtensions
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
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
|
||||
downloadReceiver.register()
|
||||
|
||||
val downloadUri = Uri.parse(url)
|
||||
val downloadUri = url.toUri()
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
@@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param pkgName The package name of the extension to uninstall
|
||||
*/
|
||||
fun uninstallApk(pkgName: String) {
|
||||
val packageUri = Uri.parse("package:$pkgName")
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
context.startActivity(intent)
|
||||
|
||||
@@ -13,7 +13,10 @@ import androidx.webkit.WebViewClientCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -42,9 +45,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
@Synchronized
|
||||
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
|
||||
|
||||
val originalRequest = chain.request()
|
||||
val response = chain.proceed(originalRequest)
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
@@ -85,7 +96,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
webview.settings.javaScriptEnabled = true
|
||||
webview.setDefaultSettings()
|
||||
|
||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
webview.settings.userAgentString = request.header("User-Agent")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
@@ -28,13 +29,15 @@ import timber.log.Timber
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource {
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
||||
|
||||
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 LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
const val ID = 0L
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
val dir = getBaseDirectories(context).firstOrNull()
|
||||
@@ -73,9 +76,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
|
||||
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()
|
||||
.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 }
|
||||
|
||||
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 fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
getBaseDirectories(context)
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
.flatten()
|
||||
.firstOrNull { it.extension == "json" }
|
||||
@@ -154,6 +202,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
?: manga.genre
|
||||
manga.status = json["status"]?.asInt ?: manga.status
|
||||
}
|
||||
|
||||
return Observable.just(manga)
|
||||
}
|
||||
|
||||
@@ -204,8 +253,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
var chapterNameIndex = 0
|
||||
var mangaTitleIndex = 0
|
||||
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||
val chapterChar = chapterName.get(chapterNameIndex)
|
||||
val mangaChar = mangaTitle.get(mangaTitleIndex)
|
||||
val chapterChar = chapterName[chapterNameIndex]
|
||||
val mangaChar = mangaTitle[mangaTitleIndex]
|
||||
if (!chapterChar.equals(mangaChar, true)) {
|
||||
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||
@@ -235,7 +284,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -269,8 +318,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
return when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -31,7 +32,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@@ -52,7 +52,7 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
// SY -->
|
||||
// Recreate sources when they change
|
||||
prefs.enableExhentai().asFlow().onEach {
|
||||
prefs.enableExhentai().asImmediateFlow {
|
||||
createEHSources().forEach { registerSource(it) }
|
||||
}.launchIn(scope)
|
||||
|
||||
@@ -72,6 +72,10 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
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>()
|
||||
|
||||
// SY -->
|
||||
@@ -98,7 +102,7 @@ open class SourceManager(private val context: Context) {
|
||||
XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
||||
val enhancedSource = EnhancedHttpSource(
|
||||
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 }
|
||||
currentDelegatedSources.plusAssign(map)
|
||||
@@ -132,12 +136,11 @@ open class SourceManager(private val context: Context) {
|
||||
if (prefs.enableExhentai().get()) {
|
||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
||||
}
|
||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
|
||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
|
||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en, context)
|
||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it, context)
|
||||
exSrcs += NHentai(context)
|
||||
exSrcs += Hitomi()
|
||||
exSrcs += EightMuses()
|
||||
exSrcs += HBrowse()
|
||||
exSrcs += Hitomi(context)
|
||||
exSrcs += EightMuses(context)
|
||||
return exSrcs
|
||||
}
|
||||
// SY <--
|
||||
@@ -196,7 +199,13 @@ open class SourceManager(private val context: Context) {
|
||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||
MangaDex::class,
|
||||
true
|
||||
)*/
|
||||
)*/,
|
||||
DelegatedSource(
|
||||
"HBrowse",
|
||||
1401584337232758222,
|
||||
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
||||
HBrowse::class
|
||||
)
|
||||
).associateBy { it.originalSourceQualifiedClassName }
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
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
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
@@ -22,27 +23,40 @@ interface SManga : Serializable {
|
||||
|
||||
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) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank()) {
|
||||
title = other.title
|
||||
title = other.originalTitle
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
author = /* SY --> */ other.originalAuthor /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.artist
|
||||
artist = /* SY --> */ other.originalArtist /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.description
|
||||
description = /* SY --> */ other.originalDescription /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.genre
|
||||
genre = /* SY --> */ other.originalGenre /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
@@ -61,9 +75,6 @@ interface SManga : Serializable {
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
// SY -->
|
||||
const val RECOMMENDS = 69 // nice
|
||||
// SY <--
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
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.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.source.EnhancedHttpSource
|
||||
import kotlin.reflect.KClass
|
||||
import rx.Completable
|
||||
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 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.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.debug.DebugToggles
|
||||
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.TAG_TYPE_LIGHT
|
||||
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.toGenreString
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.metadata.nullIfBlank
|
||||
import exh.metadata.parseHumanReadableByteCount
|
||||
import exh.ui.login.LoginController
|
||||
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.asObservableWithAsyncStacktrace
|
||||
import exh.util.ignore
|
||||
import exh.util.nullIfBlank
|
||||
import exh.util.trimOrNull
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.net.URLEncoder
|
||||
import java.util.ArrayList
|
||||
@@ -91,9 +96,9 @@ class EHentai(
|
||||
/**
|
||||
* 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)
|
||||
val parsedMangas = select(".itg > tbody > tr").filter {
|
||||
// Do not parse header and ads
|
||||
@@ -102,8 +107,11 @@ class EHentai(
|
||||
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||
val column2 = it.selectFirst(".gl3e, .gl2c")
|
||||
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||
val infoElement = it.selectFirst(".gl3e")
|
||||
|
||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||
val infoElements = infoElement?.select("div")
|
||||
val parsedTags = mutableListOf<RaisedTag>()
|
||||
|
||||
ParsedManga(
|
||||
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
||||
@@ -116,7 +124,78 @@ class EHentai(
|
||||
// Get image
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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) {}
|
||||
@@ -271,7 +393,7 @@ class EHentai(
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
@@ -477,6 +599,8 @@ class EHentai(
|
||||
element.text().trim(),
|
||||
if (element.hasClass("gtl")) {
|
||||
TAG_TYPE_LIGHT
|
||||
} else if (element.hasClass("gtw")) {
|
||||
TAG_TYPE_WEAK
|
||||
} else {
|
||||
TAG_TYPE_NORMAL
|
||||
}
|
||||
@@ -550,7 +674,7 @@ class EHentai(
|
||||
page++
|
||||
} while (parsed.second)
|
||||
|
||||
return Pair(result as List<ParsedManga>, favNames!!)
|
||||
return Pair(result.toList(), favNames!!)
|
||||
}
|
||||
|
||||
fun spPref() = if (exh) {
|
||||
@@ -832,10 +956,16 @@ class EHentai(
|
||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): EHentaiDescriptionAdapter {
|
||||
return EHentaiDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||
private const val TR_SUFFIX = "TR"
|
||||
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 val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
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.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@@ -41,7 +44,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
/**
|
||||
* 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()
|
||||
|
||||
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 fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return urlImportFetchSearchManga(query) {
|
||||
return urlImportFetchSearchManga(context, query) {
|
||||
val splitQuery = query.split(" ")
|
||||
|
||||
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
|
||||
@@ -303,9 +306,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
val titleElement = doc.selectFirst("h1")
|
||||
title = titleElement.text()
|
||||
thumbnail_url = "https:" + if (prefs.eh_hl_useHighQualityThumbs().get()) {
|
||||
doc.selectFirst("img").attr("data-srcset").substringBefore(' ')
|
||||
doc.selectFirst("img").attr("srcset").substringBefore(' ')
|
||||
} else {
|
||||
doc.selectFirst("img").attr("data-src")
|
||||
doc.selectFirst("img").attr("src")
|
||||
}
|
||||
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 = "))
|
||||
return json["files"].array.mapIndexed { index, jsonElement ->
|
||||
val hash = jsonElement["hash"].string
|
||||
val ext = if (jsonElement["haswebp"].string == "0") jsonElement["name"].string.split('.').last() else "webp"
|
||||
val path = if (jsonElement["haswebp"].string == "0") "images" 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" || !prefs.hitomiAlwaysWebp().get()) "images" else "webp"
|
||||
val hashPath1 = hash.takeLast(1)
|
||||
val hashPath2 = hash.takeLast(3).take(2)
|
||||
Page(
|
||||
@@ -421,6 +424,10 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): HitomiDescriptionAdapter {
|
||||
return HitomiDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
||||
private val PAGE_SIZE = 25
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@@ -11,7 +12,7 @@ import exh.source.DelegatedHttpSource
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import rx.Observable
|
||||
|
||||
class MangaDex(delegate: HttpSource) :
|
||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
ConfigurableSource,
|
||||
UrlImportableSource {
|
||||
@@ -19,7 +20,7 @@ class MangaDex(delegate: HttpSource) :
|
||||
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
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.ui.metadata.adapters.NHentaiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
@@ -37,7 +40,7 @@ import rx.Observable
|
||||
* 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 fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
@@ -57,7 +60,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
"$baseUrl/g/$trimmedIdQuery/"
|
||||
} else query
|
||||
|
||||
return urlImportFetchSearchManga(newQuery) {
|
||||
return urlImportFetchSearchManga(context, newQuery) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
@@ -195,7 +198,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
tags.clear()
|
||||
}?.forEach {
|
||||
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]}/"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): NHentaiDescriptionAdapter {
|
||||
return NHentaiDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
||||
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.ParsedHttpSource
|
||||
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.chapter.ChapterRecognition
|
||||
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.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
@@ -33,7 +36,7 @@ import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
|
||||
// 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(),
|
||||
LewdSource<PervEdenSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -64,7 +67,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
@@ -357,6 +360,10 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
return newUri.toString()
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
|
||||
return PervEdenDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kizitonwose.time.hours
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
||||
import exh.util.CachedField
|
||||
import exh.util.NakedTrie
|
||||
import exh.util.await
|
||||
@@ -41,7 +44,7 @@ import rx.schedulers.Schedulers
|
||||
|
||||
typealias SiteMap = NakedTrie<Unit>
|
||||
|
||||
class EightMuses :
|
||||
class EightMuses(val context: Context) :
|
||||
HttpSource(),
|
||||
LewdSource<EightMusesSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -177,7 +180,7 @@ class EightMuses :
|
||||
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return urlImportFetchSearchManga(query) {
|
||||
return urlImportFetchSearchManga(context, query) {
|
||||
fetchListing(searchMangaRequest(page, query, filters), false)
|
||||
}
|
||||
}
|
||||
@@ -274,7 +277,7 @@ class EightMuses :
|
||||
// Request
|
||||
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 out = mutableListOf<SChapter>()
|
||||
@@ -396,4 +399,8 @@ class EightMuses :
|
||||
}
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
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.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.search.Namespace
|
||||
import exh.search.SearchEngine
|
||||
import exh.search.Text
|
||||
import exh.util.await
|
||||
import exh.util.dropBlank
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
|
||||
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.Element
|
||||
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 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()
|
||||
.add("Cookie", BASE_COOKIES)
|
||||
|
||||
private val clientWithoutCookies = client.newBuilder()
|
||||
.cookieJar(CookieJar.NO_COOKIES)
|
||||
.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")
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
val tables = parseIntoTables(input)
|
||||
with(metadata) {
|
||||
hbId = Uri.parse(input.location()).pathSegments.first().toLong()
|
||||
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
|
||||
|
||||
hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong()
|
||||
|
||||
tags.clear()
|
||||
(tables[""]!! + tables["categories"]!!).forEach { (k, v) ->
|
||||
((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) ->
|
||||
when (val lowercaseNs = k.toLowerCase()) {
|
||||
"title" -> title = v.text()
|
||||
"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>> {
|
||||
return doc.select("#main > .listTable").map { ele ->
|
||||
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
|
||||
@@ -378,602 +76,16 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
||||
}.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(
|
||||
"www.hbrowse.com",
|
||||
"hbrowse.com"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
return "$baseUrl/${uri.pathSegments.first()}"
|
||||
return "/${uri.pathSegments.first()}/c00001/"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PAGE_LIST_REGEX = Regex("list *= *(\\[.*]);")
|
||||
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")
|
||||
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
||||
return HBrowseDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.HentaiCafeSearchMetadata
|
||||
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class HentaiCafe(delegate: HttpSource) :
|
||||
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HentaiCafeSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -34,7 +37,7 @@ class HentaiCafe(delegate: HttpSource) :
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
@@ -110,4 +113,8 @@ class HentaiCafe(delegate: HttpSource) :
|
||||
"https://hentai.cafe/$lcFirstPathSegment"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
|
||||
return HentaiCafeDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.PururinSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.PururinDescriptionAdapter
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class Pururin(delegate: HttpSource) :
|
||||
class Pururin(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PururinSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -38,7 +42,7 @@ class Pururin(delegate: HttpSource) :
|
||||
"$baseUrl/gallery/$trimmedIdQuery/-"
|
||||
} else query
|
||||
|
||||
return urlImportFetchSearchManga(newQuery) {
|
||||
return urlImportFetchSearchManga(context, newQuery) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
@@ -88,10 +92,11 @@ class Pururin(delegate: HttpSource) :
|
||||
else -> {
|
||||
value.select("a").forEach { link ->
|
||||
val searchUrl = Uri.parse(link.attr("href"))
|
||||
val namespace = searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2]
|
||||
tags += RaisedTag(
|
||||
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
|
||||
namespace,
|
||||
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? {
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class Tsumino(delegate: HttpSource) :
|
||||
class Tsumino(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<TsuminoSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -27,13 +33,13 @@ class Tsumino(delegate: HttpSource) :
|
||||
override val lang = "en"
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
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") {
|
||||
return null
|
||||
}
|
||||
@@ -57,9 +63,12 @@ class Tsumino(delegate: HttpSource) :
|
||||
title = it.trim()
|
||||
}
|
||||
|
||||
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let {
|
||||
tags.add(RaisedTag("artist", it, TAG_TYPE_VIRTUAL))
|
||||
artist = it
|
||||
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { artistString ->
|
||||
artistString.split("|").trimAll().dropBlank().forEach {
|
||||
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 {
|
||||
@@ -76,6 +85,12 @@ class Tsumino(delegate: HttpSource) :
|
||||
|
||||
input.getElementById("Rating")?.text()?.let {
|
||||
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 {
|
||||
@@ -85,18 +100,19 @@ class Tsumino(delegate: HttpSource) :
|
||||
|
||||
input.getElementById("Collection")?.children()?.first()?.text()?.let {
|
||||
collection = it.trim()
|
||||
tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
|
||||
input.getElementById("Group")?.children()?.first()?.text()?.let {
|
||||
group = it.trim()
|
||||
tags.add(RaisedTag("group", it, TAG_TYPE_VIRTUAL))
|
||||
tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
|
||||
val newParody = mutableListOf<String>()
|
||||
input.getElementById("Parody")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
newParody.add(entry)
|
||||
tags.add(RaisedTag("parody", entry, TAG_TYPE_VIRTUAL))
|
||||
tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
parody = newParody
|
||||
|
||||
@@ -104,14 +120,14 @@ class Tsumino(delegate: HttpSource) :
|
||||
input.getElementById("Character")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
newCharacter.add(entry)
|
||||
tags.add(RaisedTag("character", entry, TAG_TYPE_VIRTUAL))
|
||||
tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
character = newCharacter
|
||||
|
||||
input.getElementById("Tag")?.children()?.let {
|
||||
input.getElementById("Tag")?.children()?.let { tagElements ->
|
||||
tags.addAll(
|
||||
it.map {
|
||||
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
tagElements.map {
|
||||
RaisedTag("tags", it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -125,6 +141,12 @@ class Tsumino(delegate: HttpSource) :
|
||||
|
||||
companion object {
|
||||
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
|
||||
}
|
||||
|
||||
fun setTitle() {
|
||||
fun setTitle(title: String? = null) {
|
||||
var parentController = parentController
|
||||
while (parentController != null) {
|
||||
if (parentController is BaseController<*> && parentController.getTitle() != null) {
|
||||
@@ -83,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
parentController = parentController.parentController
|
||||
}
|
||||
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
|
||||
}
|
||||
|
||||
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)
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
.pushChangeHandler(FadeChangeHandler(duration))
|
||||
.popChangeHandler(FadeChangeHandler(duration))
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
|
||||
/**
|
||||
* Dismiss the dialog and pop this controller
|
||||
*/
|
||||
fun dismissDialog() {
|
||||
private fun dismissDialog() {
|
||||
if (dismissed) {
|
||||
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) {
|
||||
with(tabs) {
|
||||
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.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
@@ -22,11 +23,10 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
||||
for (i in 0 until childCount - 1) {
|
||||
val child = parent.getChildAt(i)
|
||||
val holder = parent.getChildViewHolder(child)
|
||||
if (holder is SourceHolder &&
|
||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder
|
||||
if (holder is SourceListItem &&
|
||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceListItem
|
||||
) {
|
||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||
val top = child.bottom + params.bottomMargin
|
||||
val top = child.bottom + child.marginBottom
|
||||
val bottom = top + divider.intrinsicHeight
|
||||
val left = parent.paddingStart + 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.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -75,7 +76,7 @@ open class ExtensionController :
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
||||
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
}
|
||||
|
||||
@@ -129,6 +130,9 @@ open class ExtensionController :
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
// Fixes problem with the overflow icon showing up in lieu of search
|
||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
@@ -142,9 +146,6 @@ open class ExtensionController :
|
||||
drawExtensions()
|
||||
}
|
||||
.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 {
|
||||
|
||||
-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.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import io.github.mthli.slice.Slice
|
||||
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) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SourceListItem,
|
||||
SlicedHolder {
|
||||
|
||||
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.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()
|
||||
// SY -->
|
||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase()
|
||||
// SY <--
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -91,12 +95,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||
setText(R.string.ext_update)
|
||||
}
|
||||
else -> {
|
||||
// SY -->
|
||||
if (extension.sources.any { it is ConfigurableSource }) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
text = context.getString(R.string.action_settings) + "+"
|
||||
} else {
|
||||
setText(R.string.action_settings)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
} 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.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -23,6 +26,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||
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.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
@@ -180,6 +185,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
when (item.itemId) {
|
||||
R.id.action_enable_all -> toggleAllSources(true)
|
||||
R.id.action_disable_all -> toggleAllSources(false)
|
||||
R.id.action_open_in_settings -> openInSettings()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
@@ -193,15 +199,18 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
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(
|
||||
if (enable) {
|
||||
current - source.id.toString()
|
||||
} else {
|
||||
current + source.id.toString()
|
||||
}
|
||||
)
|
||||
private fun openInSettings() {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", presenter.pkgName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
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.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -49,18 +49,18 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
||||
.launchIn(scope)
|
||||
|
||||
if (extension.isObsolete) {
|
||||
binding.extensionWarningBanner.visible()
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
|
||||
}
|
||||
|
||||
if (extension.isUnofficial) {
|
||||
binding.extensionWarningBanner.visible()
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (extension.isRedundant) {
|
||||
binding.extensionWarningBanner.visible()
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.redundant_extension_message)
|
||||
}
|
||||
// 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.LatestPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
||||
@@ -72,11 +71,7 @@ open class LatestController :
|
||||
*/
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
// Open MangaController.
|
||||
if (presenter.preferences.eh_useNewMangaInterface().get()) {
|
||||
parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction())
|
||||
} else {
|
||||
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.no_results_found
|
||||
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.source_card
|
||||
@@ -61,16 +61,16 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
|
||||
when {
|
||||
results == null -> {
|
||||
progress.visible()
|
||||
showHolder()
|
||||
progress.isVisible = true
|
||||
showResultsHolder()
|
||||
}
|
||||
results.isEmpty() -> {
|
||||
progress.gone()
|
||||
hideHolder()
|
||||
progress.isVisible = false
|
||||
showNoResults()
|
||||
}
|
||||
else -> {
|
||||
progress.gone()
|
||||
showHolder()
|
||||
progress.isVisible = false
|
||||
showResultsHolder()
|
||||
}
|
||||
}
|
||||
if (results !== lastBoundResults) {
|
||||
@@ -105,13 +105,13 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
return null
|
||||
}
|
||||
|
||||
private fun showHolder() {
|
||||
title_wrapper.visible()
|
||||
source_card.visible()
|
||||
private fun showResultsHolder() {
|
||||
no_results_found.isVisible = false
|
||||
source_card.isVisible = true
|
||||
}
|
||||
|
||||
private fun hideHolder() {
|
||||
title_wrapper.gone()
|
||||
source_card.gone()
|
||||
private fun showNoResults() {
|
||||
no_results_found.isVisible = true
|
||||
source_card.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
+3
-8
@@ -8,6 +8,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
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.ui.browse.migration.MigrationFlags
|
||||
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.extra_search_param
|
||||
import kotlinx.android.synthetic.main.migration_bottom_sheet.extra_search_param_text
|
||||
@@ -88,13 +87,9 @@ class MigrationBottomSheetDialog(
|
||||
mig_tracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||
|
||||
use_smart_search.bindToPreference(preferences.smartMigration())
|
||||
extra_search_param_text.gone()
|
||||
extra_search_param_text.isVisible = false
|
||||
extra_search_param.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
extra_search_param_text.visible()
|
||||
} else {
|
||||
extra_search_param_text.gone()
|
||||
}
|
||||
extra_search_param_text.isVisible = isChecked
|
||||
}
|
||||
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.advanced.design.PreMigrationController
|
||||
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.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
@@ -167,12 +166,12 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
val searchResult = if (useSmartSearch) {
|
||||
smartSearchEngine.smartSearch(
|
||||
source,
|
||||
mangaObj.title
|
||||
mangaObj.originalTitle
|
||||
)
|
||||
} else {
|
||||
smartSearchEngine.normalSearch(
|
||||
source,
|
||||
mangaObj.title
|
||||
mangaObj.originalTitle
|
||||
)
|
||||
}
|
||||
|
||||
@@ -222,12 +221,12 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
val searchResult = if (useSmartSearch) {
|
||||
smartSearchEngine.smartSearch(
|
||||
source,
|
||||
mangaObj.title
|
||||
mangaObj.originalTitle
|
||||
)
|
||||
} else {
|
||||
smartSearchEngine.normalSearch(
|
||||
source,
|
||||
mangaObj.title
|
||||
mangaObj.originalTitle
|
||||
)
|
||||
}
|
||||
|
||||
@@ -427,7 +426,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
private fun navigateOut() {
|
||||
if (migratingManga?.size == 1) {
|
||||
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) {
|
||||
val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let {
|
||||
db.getManga(it).executeOnIO()
|
||||
@@ -435,10 +434,9 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
if (manga != null) {
|
||||
val newStack = router.backstack.filter {
|
||||
it.controller() !is MangaController &&
|
||||
it.controller() !is MangaAllInOneController &&
|
||||
it.controller() !is MigrationListController &&
|
||||
it.controller() !is PreMigrationController
|
||||
} + if (preferences.eh_useNewMangaInterface().get()) MangaAllInOneController(manga).withFadeTransaction() else MangaController(manga).withFadeTransaction()
|
||||
} + MangaController(manga).withFadeTransaction()
|
||||
router.setBackstack(newStack, FadeChangeHandler())
|
||||
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.ui.browse.migration.MigrationFlags
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -135,7 +136,10 @@ class MigrationProcessAdapter(
|
||||
// Update favorite status
|
||||
if (replace) {
|
||||
prevManga.favorite = false
|
||||
manga.date_added = prevManga.date_added
|
||||
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||
} else {
|
||||
manga.date_added = Date().time
|
||||
}
|
||||
manga.favorite = true
|
||||
|
||||
|
||||
+19
-32
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.google.gson.Gson
|
||||
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.glide.GlideApp
|
||||
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.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
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.util.lang.launchUI
|
||||
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.visible
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import java.text.DecimalFormat
|
||||
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.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@@ -78,28 +74,19 @@ class MigrationProcessHolder(
|
||||
.attr.colorOnPrimary
|
||||
)
|
||||
)
|
||||
migration_menu.invisible()
|
||||
skip_manga.visible()
|
||||
migration_menu.isInvisible = true
|
||||
skip_manga.isVisible = true
|
||||
migration_manga_card_to.resetManga()
|
||||
if (manga != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
migration_manga_card_from.attachManga(manga, source)
|
||||
migration_manga_card_from.setOnClickListener {
|
||||
if (Injekt.get<PreferencesHelper>().eh_useNewMangaInterface().get()) {
|
||||
adapter.controller.router.pushController(
|
||||
MangaAllInOneController(
|
||||
manga,
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
} else {
|
||||
adapter.controller.router.pushController(
|
||||
MangaController(
|
||||
manga,
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
adapter.controller.router.pushController(
|
||||
MangaController(
|
||||
manga,
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,12 +123,12 @@ class MigrationProcessHolder(
|
||||
)
|
||||
}
|
||||
} 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
|
||||
.getString(R.string.no_alternatives_found)
|
||||
}
|
||||
migration_menu.visible()
|
||||
skip_manga.gone()
|
||||
migration_menu.isVisible = true
|
||||
skip_manga.isVisible = false
|
||||
adapter.sourceFinished()
|
||||
}
|
||||
}
|
||||
@@ -149,18 +136,18 @@ class MigrationProcessHolder(
|
||||
}
|
||||
|
||||
private fun View.resetManga() {
|
||||
loading_group.visible()
|
||||
loading_group.isVisible = true
|
||||
thumbnail.setImageDrawable(null)
|
||||
title.text = ""
|
||||
manga_source_label.text = ""
|
||||
manga_chapters.text = ""
|
||||
manga_chapters.gone()
|
||||
manga_chapters.isVisible = false
|
||||
manga_last_chapter_label.text = ""
|
||||
migration_manga_card_to.setOnClickListener(null)
|
||||
}
|
||||
|
||||
private fun View.attachManga(manga: Manga, source: Source) {
|
||||
loading_group.gone()
|
||||
loading_group.isVisible = false
|
||||
GlideApp.with(view.context.applicationContext)
|
||||
.load(manga.toMangaThumbnail())
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
@@ -171,10 +158,10 @@ class MigrationProcessHolder(
|
||||
title.text = if (manga.title.isBlank()) {
|
||||
view.context.getString(R.string.unknown)
|
||||
} else {
|
||||
manga.title
|
||||
manga.originalTitle
|
||||
}
|
||||
|
||||
gradient.visible()
|
||||
gradient.isVisible = true
|
||||
manga_source_label.text = if (source.id == MERGED_SOURCE_ID) {
|
||||
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
|
||||
sourceManager.getOrStub(it.source).toString()
|
||||
@@ -184,7 +171,7 @@ class MigrationProcessHolder(
|
||||
}
|
||||
|
||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||
manga_chapters.visible()
|
||||
manga_chapters.isVisible = true
|
||||
manga_chapters.text = mangaChapters.size.toString()
|
||||
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class MangaHolder(
|
||||
|
||||
fun bind(item: MangaItem) {
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
title.text = item.manga.originalTitle
|
||||
|
||||
// Create thumbnail onclick to simulate long click
|
||||
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.databinding.MigrationMangaControllerBinding
|
||||
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.source.SourceDividerItemDecoration
|
||||
import uy.kohesive.injekt.Injekt
|
||||
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.util.chapter.syncChaptersWithSource
|
||||
import exh.debug.DebugFunctions.sourceManager
|
||||
import java.util.Date
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
@@ -34,7 +35,7 @@ class MigrationMangaPresenter(
|
||||
|
||||
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
|
||||
return library.filter { it.source == sourceId }
|
||||
.sortedBy { it.title }
|
||||
.sortedBy { it.originalTitle }
|
||||
.map { MangaItem(it) }
|
||||
}
|
||||
|
||||
@@ -101,7 +102,11 @@ class MigrationMangaPresenter(
|
||||
// Update favorite status
|
||||
if (replace) {
|
||||
prevManga.favorite = false
|
||||
manga.date_added = prevManga.date_added
|
||||
prevManga.date_added = 0
|
||||
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||
} else {
|
||||
manga.date_added = Date().time
|
||||
}
|
||||
manga.favorite = true
|
||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
class SearchController(
|
||||
private var manga: Manga? = null,
|
||||
private var sources: List<CatalogueSource>? = null
|
||||
) : GlobalSearchController(manga?.title) {
|
||||
) : GlobalSearchController(manga?.originalTitle) {
|
||||
|
||||
private var newManga: Manga? = null
|
||||
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.withFadeTransaction
|
||||
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.manga.MigrationMangaController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import exh.util.await
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
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 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.source_browse
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
||||
|
||||
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SourceListItem,
|
||||
SlicedHolder {
|
||||
|
||||
override val slice = Slice(card).apply {
|
||||
@@ -23,15 +25,15 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||
override val viewToSlice: View
|
||||
get() = card
|
||||
|
||||
// SY -->
|
||||
init {
|
||||
source_latest.gone()
|
||||
// SY -->
|
||||
source_browse.text = "All"
|
||||
source_browse.setOnClickListener {
|
||||
source_latest.isVisible = true
|
||||
source_latest.text = view.context.getString(R.string.all)
|
||||
source_latest.setOnClickListener {
|
||||
adapter.allClickListener?.onAllClick(bindingAdapterPosition)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun bind(item: SourceItem) {
|
||||
val source = item.source
|
||||
|
||||
@@ -22,26 +22,15 @@ class SourceAdapter(val controller: SourceController) :
|
||||
/**
|
||||
* Listener for browse item clicks.
|
||||
*/
|
||||
val browseClickListener: OnBrowseClickListener = controller
|
||||
|
||||
/**
|
||||
* Listener for latest item clicks.
|
||||
*/
|
||||
val latestClickListener: OnLatestClickListener = controller
|
||||
val clickListener: OnSourceClickListener = controller
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks browse.
|
||||
* Note: Should only be handled by [SourceController]
|
||||
*/
|
||||
interface OnBrowseClickListener {
|
||||
interface OnSourceClickListener {
|
||||
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 onPinClick(position: Int)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
@@ -19,14 +20,18 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
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.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
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.globalsearch.GlobalSearchController
|
||||
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 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
|
||||
*/
|
||||
class SourceController(bundle: Bundle? = null) :
|
||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
SourceAdapter.OnBrowseClickListener,
|
||||
SourceAdapter.OnLatestClickListener,
|
||||
ChangeSourceCategoriesDialog.Listener {
|
||||
SourceAdapter.OnSourceClickListener,
|
||||
/*SY -->*/ ChangeSourceCategoriesDialog.Listener /*SY <--*/ {
|
||||
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
@@ -171,11 +175,16 @@ class SourceController(bundle: Bundle? = null) :
|
||||
val items = mutableListOf(
|
||||
Pair(
|
||||
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) {
|
||||
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 -->
|
||||
@@ -198,31 +207,21 @@ class SourceController(bundle: Bundle? = null) :
|
||||
)
|
||||
// SY <--
|
||||
|
||||
MaterialDialog(activity)
|
||||
.title(text = item.source.name)
|
||||
.listItems(
|
||||
items = items.map { it.first },
|
||||
waitForPositiveButton = false
|
||||
) { dialog, which, _ ->
|
||||
items[which].second()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
SourceOptionsDialog(item, items).showDialog(router)
|
||||
}
|
||||
|
||||
private fun disableSource(source: Source) {
|
||||
val current = preferences.disabledSources().get()
|
||||
preferences.disabledSources().set(current + source.id.toString())
|
||||
preferences.disabledSources() += source.id.toString()
|
||||
|
||||
presenter.updateSources()
|
||||
}
|
||||
|
||||
private fun pinSource(source: Source, isPinned: Boolean) {
|
||||
val current = preferences.pinnedSources().get()
|
||||
private fun toggleSourcePin(source: Source) {
|
||||
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
||||
if (isPinned) {
|
||||
preferences.pinnedSources().set(current - source.id.toString())
|
||||
preferences.pinnedSources() -= source.id.toString()
|
||||
} else {
|
||||
preferences.pinnedSources().set(current + source.id.toString())
|
||||
preferences.pinnedSources() += source.id.toString()
|
||||
}
|
||||
|
||||
presenter.updateSources()
|
||||
@@ -230,16 +229,14 @@ class SourceController(bundle: Bundle? = null) :
|
||||
|
||||
// SY -->
|
||||
private fun watchCatalogue(source: Source, isWatched: Boolean) {
|
||||
val current = preferences.latestTabSources().get()
|
||||
|
||||
if (isWatched) {
|
||||
preferences.latestTabSources().set(current - source.id.toString())
|
||||
preferences.latestTabSources() -= source.id.toString()
|
||||
} else {
|
||||
if (current.size + 1 !in 0..5) {
|
||||
if (preferences.latestTabSources().get().size + 1 !in 0..5) {
|
||||
applicationContext?.toast(R.string.too_many_watched)
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -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 -->
|
||||
@Parcelize
|
||||
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.PreferenceScreen
|
||||
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.icon
|
||||
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.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
|
||||
import exh.source.BlacklistedSources
|
||||
import java.util.TreeMap
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -34,9 +35,7 @@ class SourceFilterController : SettingsController() {
|
||||
}
|
||||
|
||||
private val onlineSources by lazy {
|
||||
Injekt.get<SourceManager>().getOnlineSources().filter {
|
||||
it.id !in BlacklistedSources.HIDDEN_SOURCES
|
||||
}
|
||||
Injekt.get<SourceManager>().getVisibleOnlineSources()
|
||||
}
|
||||
|
||||
private var query = ""
|
||||
@@ -80,12 +79,11 @@ class SourceFilterController : SettingsController() {
|
||||
|
||||
onChange { newValue ->
|
||||
val checked = newValue as Boolean
|
||||
val current = preferences.enabledLanguages().get()
|
||||
if (!checked) {
|
||||
preferences.enabledLanguages().set(current - lang)
|
||||
preferences.enabledLanguages() -= lang
|
||||
removeAll()
|
||||
} else {
|
||||
preferences.enabledLanguages().set(current + lang)
|
||||
preferences.enabledLanguages() += lang
|
||||
addLanguageSources(this, sortedSources(sourcesByLang[lang]))
|
||||
}
|
||||
true
|
||||
@@ -147,15 +145,12 @@ class SourceFilterController : SettingsController() {
|
||||
|
||||
onChange { newValue ->
|
||||
val checked = newValue as Boolean
|
||||
val current = preferences.disabledSources().get()
|
||||
|
||||
preferences.disabledSources().set(
|
||||
if (checked) {
|
||||
current - id
|
||||
} else {
|
||||
current + id
|
||||
}
|
||||
)
|
||||
if (checked) {
|
||||
preferences.disabledSources() -= id
|
||||
} else {
|
||||
preferences.disabledSources() += id
|
||||
}
|
||||
|
||||
group.removeAll()
|
||||
addLanguageSources(group, sortedSources(sources))
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
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.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.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),
|
||||
SourceListItem,
|
||||
SlicedHolder {
|
||||
|
||||
override val slice = Slice(card).apply {
|
||||
@@ -27,18 +30,17 @@ class SourceHolder(view: View, override val adapter: SourceAdapter /* SY --> */,
|
||||
get() = card
|
||||
|
||||
init {
|
||||
source_browse.setOnClickListener {
|
||||
adapter.browseClickListener.onBrowseClick(bindingAdapterPosition)
|
||||
source_latest.setOnClickListener {
|
||||
adapter.clickListener.onLatestClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
source_latest.setOnClickListener {
|
||||
adapter.latestClickListener.onLatestClick(bindingAdapterPosition)
|
||||
pin.setOnClickListener {
|
||||
adapter.clickListener.onPinClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (!showButtons) {
|
||||
source_browse.gone()
|
||||
source_latest.gone()
|
||||
source_latest.isVisible = false
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
@@ -59,11 +61,13 @@ class SourceHolder(view: View, override val adapter: SourceAdapter /* SY --> */,
|
||||
}
|
||||
}
|
||||
|
||||
source_browse.setText(R.string.browse)
|
||||
if (source.supportsLatest /* SY --> */ && showButtons /* SY <-- */) {
|
||||
source_latest.visible()
|
||||
source_latest.isVisible = source.supportsLatest/* SY --> */ && showButtons /* SY <-- */
|
||||
|
||||
pin.isVisible = showButtons
|
||||
if (item.isPinned) {
|
||||
pin.setVectorCompat(R.drawable.ic_push_pin_filled_24dp, view.context.getResourceColor(R.attr.colorAccent))
|
||||
} 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 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) {
|
||||
|
||||
/**
|
||||
@@ -42,4 +49,15 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null
|
||||
) {
|
||||
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 {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source ->
|
||||
if (source.id.toString() in pinnedSourceIds) {
|
||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), controllerMode == SourceController.Mode.CATALOGUE))
|
||||
val isPinned = source.id.toString() in pinnedSourceIds
|
||||
if (isPinned) {
|
||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned, controllerMode == SourceController.Mode.CATALOGUE))
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -106,6 +107,7 @@ class SourcePresenter(
|
||||
SourceItem(
|
||||
source,
|
||||
LangItem("custom|" + SourceAndCategory.second),
|
||||
isPinned,
|
||||
controllerMode == SourceController.Mode.CATALOGUE
|
||||
)
|
||||
)
|
||||
@@ -115,7 +117,7 @@ class SourcePresenter(
|
||||
}
|
||||
// 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) {
|
||||
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) }
|
||||
}
|
||||
|
||||
|
||||
+65
-56
@@ -10,6 +10,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -18,6 +19,7 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.input.input
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
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.model.FilterList
|
||||
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.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
|
||||
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.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.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
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.shrinkOnScroll
|
||||
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.EmptyView
|
||||
import exh.EXHSavedSearch
|
||||
import exh.isEhBasedSource
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
@@ -64,7 +64,6 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -72,6 +71,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*/
|
||||
open class BrowseSourceController(bundle: Bundle) :
|
||||
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||
FabController,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
@@ -113,6 +113,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -146,7 +149,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
// SY -->
|
||||
return when (mode) {
|
||||
Mode.CATALOGUE -> presenter.source.name
|
||||
Mode.RECOMMENDS -> recommendsConfig!!.manga.title
|
||||
Mode.RECOMMENDS -> recommendsConfig!!.title
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
@@ -156,8 +159,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
return BrowseSourcePresenter(
|
||||
args.getLong(SOURCE_ID_KEY),
|
||||
args.getString(SEARCH_QUERY_KEY),
|
||||
searchManga = if (mode == Mode.RECOMMENDS) recommendsConfig?.manga else null,
|
||||
recommends = (mode == Mode.RECOMMENDS)
|
||||
recommendsMangaId = if (mode == Mode.RECOMMENDS) recommendsConfig?.mangaId else null
|
||||
)
|
||||
// SY <--
|
||||
}
|
||||
@@ -177,7 +179,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
adapter = FlexibleAdapter(null, this)
|
||||
setupRecycler(view)
|
||||
|
||||
binding.progress.visible()
|
||||
binding.progress.isVisible = true
|
||||
}
|
||||
|
||||
open fun initFilterSheet() {
|
||||
@@ -189,7 +191,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
// SY -->
|
||||
binding.fabFilter.text = activity!!.getString(R.string.eh_saved_searches)
|
||||
actionFab?.text = activity!!.getString(R.string.saved_searches)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -214,8 +216,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
onSaveClicked = {
|
||||
filterSheet?.context?.let {
|
||||
MaterialDialog(it)
|
||||
.title(text = "Save current search query?")
|
||||
.input("My search name", hintRes = null) { _, searchName ->
|
||||
.title(R.string.save_search)
|
||||
.input(hintRes = R.string.save_search_hint) { _, searchName ->
|
||||
val oldSavedSearches = presenter.loadSearches()
|
||||
if (searchName.isNotBlank() &&
|
||||
oldSavedSearches.size < MAX_SAVED_SEARCHES
|
||||
@@ -244,8 +246,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
if (search == null) {
|
||||
filterSheet?.context?.let {
|
||||
MaterialDialog(it)
|
||||
.title(text = "Failed to load saved searches!")
|
||||
.message(text = "An error occurred while loading your saved searches.")
|
||||
.title(R.string.save_search_failed_to_load)
|
||||
.message(R.string.save_search_failed_to_load_message)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
@@ -271,8 +273,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
if (search == null || search.name != name) {
|
||||
filterSheet?.context?.let {
|
||||
MaterialDialog(it)
|
||||
.title(text = "Failed to delete saved search!")
|
||||
.message(text = "An error occurred while deleting the search.")
|
||||
.title(R.string.save_search_failed_to_delete)
|
||||
.message(R.string.save_search_failed_to_delete_message)
|
||||
.cancelable(true)
|
||||
.cancelOnTouchOutside(true)
|
||||
.show()
|
||||
@@ -282,10 +284,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
|
||||
filterSheet?.context?.let {
|
||||
MaterialDialog(it)
|
||||
.title(text = "Delete saved search query?")
|
||||
.message(text = "Are you sure you wish to delete your saved search query: '${search.name}'?")
|
||||
.title(R.string.save_search_delete)
|
||||
.message(text = it.getString(R.string.save_search_delete_message, search.name))
|
||||
.positiveButton(R.string.action_cancel)
|
||||
.negativeButton(text = "Confirm") {
|
||||
.negativeButton(android.R.string.yes) {
|
||||
val newSearches = savedSearches.filterIndexed { index, _ ->
|
||||
index != indexToDelete
|
||||
}
|
||||
@@ -299,17 +301,30 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
}
|
||||
// EXH <--
|
||||
)
|
||||
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
|
||||
filterSheet?.setOnShowListener { binding.fabFilter.gone() }
|
||||
filterSheet?.setOnDismissListener { binding.fabFilter.visible() }
|
||||
filterSheet?.setOnShowListener { actionFab?.isVisible = false }
|
||||
filterSheet?.setOnDismissListener { actionFab?.isVisible = true }
|
||||
|
||||
binding.fabFilter.setOnClickListener { filterSheet?.show() }
|
||||
actionFab?.setOnClickListener { filterSheet?.show() }
|
||||
|
||||
binding.fabFilter.offsetAppbarHeight(activity!!)
|
||||
binding.fabFilter.visible()
|
||||
actionFab?.isVisible = true
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -333,7 +348,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
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 {
|
||||
id = R.id.recycler
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
@@ -369,7 +384,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
)
|
||||
recycler.clipToPadding = false
|
||||
|
||||
binding.fabFilter.shrinkOnScroll(recycler)
|
||||
actionFab?.shrinkOnScroll(recycler)
|
||||
}
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
@@ -386,12 +401,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.source_browse, menu)
|
||||
|
||||
// SY -->
|
||||
if (mode == Mode.RECOMMENDS) {
|
||||
menu.findItem(R.id.action_search).isVisible = false
|
||||
}
|
||||
// SY <--
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
@@ -424,6 +433,15 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
DisplayMode.LIST -> R.id.action_list
|
||||
}
|
||||
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) {
|
||||
@@ -624,8 +642,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
presenter.refreshDisplayMode()
|
||||
activity?.invalidateOptionsMenu()
|
||||
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 {
|
||||
(adapter.getItem(it) as? SourceItem)?.manga
|
||||
}
|
||||
@@ -670,7 +689,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
*/
|
||||
private fun showProgressBar() {
|
||||
binding.emptyView.hide()
|
||||
binding.progress.visible()
|
||||
binding.progress.isVisible = true
|
||||
snack?.dismiss()
|
||||
snack = null
|
||||
}
|
||||
@@ -680,7 +699,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
*/
|
||||
private fun hideProgressBar() {
|
||||
binding.emptyView.hide()
|
||||
binding.progress.gone()
|
||||
binding.progress.isVisible = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,25 +713,15 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
// SY -->
|
||||
when (mode) {
|
||||
Mode.CATALOGUE -> {
|
||||
if (preferences.eh_useNewMangaInterface().get()) {
|
||||
router.pushController(
|
||||
MangaAllInOneController(
|
||||
item.manga,
|
||||
true,
|
||||
args.getParcelable(SMART_SEARCH_CONFIG_KEY)
|
||||
).withFadeTransaction()
|
||||
)
|
||||
} else {
|
||||
router.pushController(
|
||||
MangaController(
|
||||
item.manga,
|
||||
true,
|
||||
args.getParcelable(SMART_SEARCH_CONFIG_KEY)
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
router.pushController(
|
||||
MangaController(
|
||||
item.manga,
|
||||
true,
|
||||
args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA)
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
Mode.RECOMMENDS -> openSmartSearch(item.manga.title)
|
||||
Mode.RECOMMENDS -> openSmartSearch(item.manga.originalTitle)
|
||||
}
|
||||
// SY <--
|
||||
return false
|
||||
@@ -821,7 +830,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
|
||||
// SY -->
|
||||
@Parcelize
|
||||
data class RecommendsConfig(val manga: Manga) : Parcelable
|
||||
data class RecommendsConfig(val title: String, val mangaId: Long?) : Parcelable
|
||||
|
||||
enum class Mode {
|
||||
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.util.removeCovers
|
||||
import exh.EXHSavedSearch
|
||||
import exh.isEhBasedSource
|
||||
import java.lang.RuntimeException
|
||||
import kotlinx.coroutines.flow.subscribe
|
||||
import java.util.Date
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@@ -56,14 +57,13 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
open class BrowseSourcePresenter(
|
||||
private val sourceId: Long,
|
||||
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 db: DatabaseHelper = Injekt.get(),
|
||||
private val prefs: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
// SY -->
|
||||
private val recommends: Boolean = false
|
||||
// SY <--
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<BrowseSourceController>() {
|
||||
|
||||
/**
|
||||
@@ -74,7 +74,7 @@ open class BrowseSourcePresenter(
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = /* SY --> */ if (recommends) "" else /* SY <-- */ searchQuery ?: ""
|
||||
var query = searchQuery ?: ""
|
||||
private set
|
||||
|
||||
/**
|
||||
@@ -113,6 +113,10 @@ open class BrowseSourcePresenter(
|
||||
*/
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
// SY -->
|
||||
private var manga: Manga? = null
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Subscription to initialize manga details.
|
||||
*/
|
||||
@@ -129,6 +133,10 @@ open class BrowseSourcePresenter(
|
||||
query = savedState.getString(::query.name, "")
|
||||
}
|
||||
|
||||
if (recommendsMangaId != null) {
|
||||
manga = db.getManga(recommendsMangaId).executeAsBlocking()
|
||||
}
|
||||
|
||||
restartPager()
|
||||
}
|
||||
|
||||
@@ -151,8 +159,8 @@ open class BrowseSourcePresenter(
|
||||
|
||||
// Create a new pager.
|
||||
// SY -->
|
||||
pager = if (recommends && searchManga != null) RecommendsPager(
|
||||
searchManga
|
||||
pager = if (recommendsMangaId != null && manga != null) RecommendsPager(
|
||||
manga ?: throw Exception("Could not get Manga")
|
||||
) else createPager(query, filters)
|
||||
// SY <--
|
||||
|
||||
@@ -164,9 +172,13 @@ open class BrowseSourcePresenter(
|
||||
pagerSubscription?.let { remove(it) }
|
||||
pagerSubscription = pager.results()
|
||||
.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) }
|
||||
.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())
|
||||
.subscribeReplay(
|
||||
{ view, (page, mangas) ->
|
||||
@@ -279,9 +291,15 @@ open class BrowseSourcePresenter(
|
||||
*/
|
||||
fun changeMangaFavorite(manga: Manga) {
|
||||
manga.favorite = !manga.favorite
|
||||
manga.date_added = when (manga.favorite) {
|
||||
true -> Date().time
|
||||
false -> 0
|
||||
}
|
||||
|
||||
if (!manga.favorite) {
|
||||
manga.removeCovers(coverCache)
|
||||
}
|
||||
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
@@ -13,9 +15,9 @@ abstract class Pager(var currentPage: Int = 1) {
|
||||
var hasNextPage = true
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -25,6 +27,11 @@ abstract class Pager(var currentPage: Int = 1) {
|
||||
val page = currentPage
|
||||
currentPage++
|
||||
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.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
|
||||
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?>) {
|
||||
holder.progressBar.gone()
|
||||
holder.progressMessage.gone()
|
||||
holder.progressBar.isVisible = false
|
||||
holder.progressMessage.isVisible = false
|
||||
|
||||
if (!adapter.isEndlessScrollEnabled) {
|
||||
loadMore = false
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
holder.progressBar.visible()
|
||||
holder.progressBar.isVisible = true
|
||||
} else {
|
||||
holder.progressMessage.visible()
|
||||
holder.progressMessage.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ open class RecommendsPager(
|
||||
|
||||
private fun getRecs(api: API) {
|
||||
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) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
+3
-4
@@ -3,11 +3,10 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
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()) :
|
||||
RecyclerView.Adapter<SavedSearchesAdapter.SavedSearchesViewHolder>() {
|
||||
@@ -29,9 +28,9 @@ class SavedSearchesAdapter(var chips: List<Chip> = emptyList()) :
|
||||
fun bind(chips: List<Chip> = emptyList()) {
|
||||
binding.savedSearches.removeAllViews()
|
||||
if (chips.isEmpty()) {
|
||||
binding.savedSearchesTitle.gone()
|
||||
binding.savedSearchesTitle.isVisible = false
|
||||
} else {
|
||||
binding.savedSearchesTitle.visible()
|
||||
binding.savedSearchesTitle.isVisible = true
|
||||
chips.forEach {
|
||||
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