diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..32d6b8a7e2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.{kt,kts}] +disabled_rules=import-ordering \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 1d77babeaf..0000000000 --- a/app/build.gradle +++ /dev/null @@ -1,273 +0,0 @@ -import java.text.SimpleDateFormat - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' -apply plugin: 'com.github.zellius.shortcut-helper' - -shortcutHelper.filePath = './shortcuts.xml' - -ext { - // Git is needed in your system PATH for these commands to work. - // If it's not installed, you can return a random value as a workaround - getCommitCount = { - return 'git rev-list --count HEAD'.execute().text.trim() - // return "1" - } - - getGitSha = { - return 'git rev-parse --short HEAD'.execute().text.trim() - // return "1" - } - - getBuildTime = { - def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") - df.setTimeZone(TimeZone.getTimeZone("UTC")) - return df.format(new Date()) - } -} - -android { - compileSdkVersion 29 - buildToolsVersion '29.0.2' - publishNonDefault true - - defaultConfig { - applicationId "eu.kanade.tachiyomi" - minSdkVersion 21 - targetSdkVersion 29 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 60 - versionName '0.9.81' - - buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" - buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" - buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" - buildConfigField "boolean", "INCLUDE_UPDATER", "false" - - vectorDrawables.useSupportLibrary = true - - multiDexEnabled true - - ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86" - } - } - - buildTypes { - debug { - versionNameSuffix "-${getCommitCount()}" - applicationIdSuffix ".debug" - } - release { - applicationIdSuffix = '.j2k' - } - } - - flavorDimensions "default" - - productFlavors { - standard { - buildConfigField "boolean", "INCLUDE_UPDATER", "true" - dimension "default" - } - fdroid { - dimension "default" - } - dev { - resConfigs "en" - dimension "default" - } - } - - packagingOptions { - exclude 'META-INF/DEPENDENCIES' - exclude 'LICENSE.txt' - exclude 'META-INF/LICENSE' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE' - } - - lintOptions { - abortOnError false - checkReleaseBuilds false - } - - compileOptions { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - } - - kotlinOptions { - jvmTarget = "1.8" - } -} - -dependencies { - - // Modified dependencies - implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7' - implementation 'com.github.inorichi:junrar-android:634c1f5' - - // Android support library - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'com.google.android.material:material:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.0' - implementation 'androidx.annotation:annotation:1.1.0' - implementation 'androidx.browser:browser:1.2.0' - implementation 'androidx.biometric:biometric:1.0.1' - - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - - implementation 'androidx.multidex:multidex:2.0.1' - - standardImplementation 'com.google.firebase:firebase-core:17.2.2' - - final lifecycle_version = "2.1.0" - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" - - // ReactiveX - implementation 'io.reactivex:rxandroid:1.2.1' - implementation 'io.reactivex:rxjava:1.3.8' - implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' - implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' - implementation 'com.github.pwittchen:reactivenetwork:0.13.0' - - // Network client - final okhttp_version = '4.3.1' - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" - implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" - implementation 'com.squareup.okio:okio:2.4.3' - - // REST - final retrofit_version = '2.7.1' - implementation "com.squareup.retrofit2:retrofit:$retrofit_version" - implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" - implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" - - // JSON - implementation 'com.google.code.gson:gson:2.8.6' - implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' - - // JavaScript engine - implementation 'com.squareup.duktape:duktape-android:1.3.0' - - // Disk - implementation 'com.jakewharton:disklrucache:2.0.2' - implementation 'com.github.inorichi:unifile:e9ee588' - - // HTML parser - implementation 'org.jsoup:jsoup:1.12.1' - - // Job scheduling - implementation 'com.evernote:android-job:1.2.5' - implementation 'com.google.android.gms:play-services-gcm:17.0.0' - - // Changelog - implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' - - // Database - implementation 'androidx.sqlite:sqlite:2.1.0' - implementation 'com.github.inorichi.storio:storio-common:8be19de@aar' - implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar' - implementation 'io.requery:sqlite-android:3.25.2' - - // Model View Presenter - final nucleus_version = '3.0.0' - implementation "info.android15.nucleus:nucleus:$nucleus_version" - implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version" - - // Dependency injection - implementation "com.github.inorichi.injekt:injekt-core:65b0440" - - // Image library - final glide_version = '4.11.0' - implementation "com.github.bumptech.glide:glide:$glide_version" - implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" - kapt "com.github.bumptech.glide:compiler:$glide_version" - - // Transformations - implementation 'jp.wasabeef:glide-transformations:4.0.0' - - // Logging - implementation 'com.jakewharton.timber:timber:4.7.1' - - // Crash reports - implementation 'ch.acra:acra:4.9.2' - - // UI - implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' - implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' - implementation 'eu.davidea:flexible-adapter:5.1.0' - implementation 'eu.davidea:flexible-adapter-ui:1.0.0' - implementation 'com.nononsenseapps:filepicker:2.5.2' - implementation 'com.github.amulyakhare:TextDrawable:558677e' - implementation 'com.afollestad.material-dialogs:core:3.1.1' - implementation 'com.afollestad.material-dialogs:input:3.1.1' - implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' - implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0' - implementation 'com.github.mthli:Slice:v1.2' - implementation 'com.github.kizitonwose:AndroidTagGroup:1.6.0' - implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a' - - // Conductor - implementation 'com.bluelinelabs:conductor:2.1.5' - implementation ("com.bluelinelabs:conductor-support:2.1.5") { - exclude group: "com.android.support" - } - implementation 'com.github.inorichi:conductor-support-preference:a32c357' - - // RxBindings - final rxbindings_version = '1.0.1' - implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" - implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" - implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" - implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version" - - // Tests - testImplementation 'junit:junit:4.12' - testImplementation 'org.assertj:assertj-core:1.7.1' - testImplementation 'org.mockito:mockito-core:1.10.19' - - final robolectric_version = '3.1.4' - testImplementation "org.robolectric:robolectric:$robolectric_version" - testImplementation "org.robolectric:shadows-multidex:$robolectric_version" - testImplementation "org.robolectric:shadows-play-services:$robolectric_version" - - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - - final coroutines_version = '1.3.2' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - - // Text distance - implementation 'info.debatty:java-string-similarity:1.2.1' -} - -buildscript { - ext.kotlin_version = '1.3.61' - repositories { - mavenCentral() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - } -} - -repositories { - mavenCentral() -} - -androidExtensions { - experimental = true -} - -if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) { - apply plugin: 'com.google.gms.google-services' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000000..7e1caf8f8e --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,252 @@ +import java.io.ByteArrayOutputStream +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +plugins { + id("com.android.application") + kotlin("android") + kotlin("android.extensions") + kotlin("kapt") + id("org.jmailen.kotlinter") version "2.3.1" + id("com.github.zellius.shortcut-helper") + id("com.google.gms.google-services") apply false +} + +fun getBuildTime() = DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now(ZoneOffset.UTC)) +fun getCommitCount() = runCommand("git rev-list --count HEAD") +fun getGitSha() = runCommand("git rev-parse --short HEAD") + +fun runCommand(command: String): String { + val byteOut = ByteArrayOutputStream() + project.exec { + commandLine = command.split(" ") + standardOutput = byteOut + } + return String(byteOut.toByteArray()).trim() +} + +android { + compileSdkVersion(29) + buildToolsVersion("29.0.2") + + defaultConfig { + minSdkVersion(23) + targetSdkVersion(29) + applicationId = "eu.kanade.tachiyomi" + versionCode = 62 + versionName = "0.9.82" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled = true + + buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") + buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") + buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"") + buildConfigField("Boolean", "INCLUDE_UPDATER", "false") + + + ndk { + abiFilters("armeabi-v7a", "arm64-v8a", "x86") + } + } + buildTypes { + getByName("debug") { + applicationIdSuffix = ".debugJ2K" + } + getByName("release") { + applicationIdSuffix = ".j2k" + } + } + + flavorDimensions("default") + + productFlavors { + create("standard") { + buildConfigField("Boolean", "INCLUDE_UPDATER", "true") + } + create("dev") { + resConfig("en") + } + } + + lintOptions { + isAbortOnError = false + isCheckReleaseBuilds = false + } + + compileOptions { + setSourceCompatibility(1.8) + setTargetCompatibility(1.8) + } + kotlinOptions { + jvmTarget = "1.8" + } +} +androidExtensions { + isExperimental = true +} + +shortcutHelper { + setFilePath("./shortcuts.xml") +} + +dependencies { +// Modified dependencies + implementation("com.github.inorichi:subsampling-scale-image-view:ac0dae7") + implementation("com.github.inorichi:junrar-android:634c1f5") + + +// Android support library + implementation("androidx.appcompat:appcompat:1.1.0") + implementation("androidx.cardview:cardview:1.0.0") + implementation("com.google.android.material:material:1.1.0") + implementation("androidx.recyclerview:recyclerview:1.1.0") + implementation("androidx.preference:preference:1.1.0") + implementation("androidx.annotation:annotation:1.1.0") + implementation("androidx.browser:browser:1.2.0") + implementation("androidx.biometric:biometric:1.0.1") + implementation("androidx.palette:palette:1.0.0") + + + implementation("androidx.constraintlayout:constraintlayout:1.1.3") + + implementation("androidx.multidex:multidex:2.0.1") + + implementation("com.google.firebase:firebase-core:17.2.3") + + val lifecycleVersion = "2.1.0" + implementation("androidx.lifecycle:lifecycle-extensions:$lifecycleVersion") + implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") + +// ReactiveX + implementation("io.reactivex:rxandroid:1.2.1") + implementation("io.reactivex:rxjava:1.3.8") + implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") + implementation("com.f2prateek.rx.preferences:rx-preferences:1.0.2") + implementation("com.github.pwittchen:reactivenetwork:0.13.0") + +// Network client + val okhttpVersion = "4.3.1" + implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") + implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") + implementation("com.squareup.okio:okio:2.4.3") + +// REST + val retrofitVersion = "2.7.1" + implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") + implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion") + +// JSON + implementation("com.google.code.gson:gson:2.8.6") + implementation("com.github.salomonbrys.kotson:kotson:2.5.0") + +// JavaScript engine + implementation("com.squareup.duktape:duktape-android:1.3.0") + +// Disk + implementation("com.jakewharton:disklrucache:2.0.2") + implementation("com.github.inorichi:unifile:e9ee588") + +// HTML parser + implementation("org.jsoup:jsoup:1.13.1") + +// Job scheduling + implementation("com.evernote:android-job:1.4.2") + implementation("com.google.android.gms:play-services-gcm:17.0.0") + +// Changelog + implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0") + +// Database + implementation("androidx.sqlite:sqlite:2.1.0") + implementation("com.github.inorichi.storio:storio-common:8be19de@aar") + implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar") + implementation("io.requery:sqlite-android:3.31.0") + +// Model View Presenter + val nucleusVersion = "3.0.0" + implementation("info.android15.nucleus:nucleus:$nucleusVersion") + implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion") + +// Dependency injection + implementation("com.github.inorichi.injekt:injekt-core:65b0440") + +// Image library + val glideVersion = "4.11.0" + implementation("com.github.bumptech.glide:glide:$glideVersion") + implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") + kapt("com.github.bumptech.glide:compiler:$glideVersion") + +// Transformations + implementation("jp.wasabeef:glide-transformations:4.1.0") + +// Logging + implementation("com.jakewharton.timber:timber:4.7.1") + +// UI + implementation("com.dmitrymalkovich.android:material-design-dimens:1.4") + implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4") + implementation("eu.davidea:flexible-adapter:5.1.0") + implementation("eu.davidea:flexible-adapter-ui:1.0.0") + implementation("com.nononsenseapps:filepicker:2.5.2") + implementation("com.github.amulyakhare:TextDrawable:558677e") + implementation("com.afollestad.material-dialogs:core:3.3.0") + implementation("com.afollestad.material-dialogs:input:3.3.0") + implementation("me.zhanghai.android.systemuihelper:library:1.0.0") + implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") + implementation("com.github.mthli:Slice:v1.2") + implementation("com.reddit:indicator-fast-scroll:1.2.1") + + implementation("com.github.kizitonwose:AndroidTagGroup:1.6.0") + implementation("com.github.chrisbanes:PhotoView:2.3.0") + implementation("com.github.carlosesco:DirectionalViewPager:a844dbca0a") + +// Conductor + implementation("com.bluelinelabs:conductor:2.1.5") + implementation("com.bluelinelabs:conductor-support:2.1.5") { + exclude("group", "com.android.support") + } + implementation("com.github.inorichi:conductor-support-preference:a32c357") + +// RxBindings + val rxbindingsVersion = "1.0.1" + implementation("com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindingsVersion") + implementation("com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindingsVersion") + implementation("com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindingsVersion") + implementation("com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindingsVersion") + +// Tests + testImplementation("junit:junit:4.13") + testImplementation("org.assertj:assertj-core:3.12.2") + testImplementation("org.mockito:mockito-core:1.10.19") + + val robolectricVersion = "3.1.4" + testImplementation("org.robolectric:robolectric:$robolectricVersion") + testImplementation("org.robolectric:shadows-multidex:$robolectricVersion") + testImplementation("org.robolectric:shadows-play-services:$robolectricVersion") + + + implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) + + val coroutinesVersion = "1.3.3" + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + + //Crash reports + val acraVersion = "4.9.2" + implementation("ch.acra:acra:$acraVersion") + + // Text distance + implementation("info.debatty:java-string-similarity:1.2.1") +} + +tasks.preBuild { + dependsOn(tasks.lintKotlin) +} +tasks.lintKotlin { + dependsOn(tasks.formatKotlin) +} + +if (gradle.startParameter.taskRequests.toString().contains("Standard")) { + apply(mapOf("plugin" to "com.google.gms.google-services")) +} \ No newline at end of file diff --git a/app/shortcuts.xml b/app/shortcuts.xml index f0d74789ea..f831c257ed 100644 --- a/app/shortcuts.xml +++ b/app/shortcuts.xml @@ -1,25 +1,15 @@ - - - + android:shortcutLongLabel="@string/recent_updates" + android:shortcutShortLabel="@string/updates"> + android:shortcutLongLabel="@string/history" + android:shortcutShortLabel="@string/history"> + android:shortcutId="show_extensions" + android:shortcutLongLabel="@string/extensions" + android:shortcutShortLabel="@string/extensions"> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 493250655a..f4cf12020a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ android:networkSecurityConfig="@xml/network_security_config"> @@ -61,7 +62,7 @@ android:name=".ui.webview.WebViewActivity" android:configChanges="uiMode|orientation|screenSize"/> + android:name=".ui.security.BiometricActivity" /> = 0) { - MainActivity.unlocked = false + SecureActivityDelegate.locked = true } } @@ -92,5 +92,4 @@ open class App : Application(), LifecycleObserver { protected open fun setupNotificationChannels() { Notifications.createChannels(this) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 4c0621a1a8..f9aa0a6dc3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -13,7 +13,11 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import uy.kohesive.injekt.api.* +import uy.kohesive.injekt.api.InjektModule +import uy.kohesive.injekt.api.InjektRegistrar +import uy.kohesive.injekt.api.addSingleton +import uy.kohesive.injekt.api.addSingletonFactory +import uy.kohesive.injekt.api.get class AppModule(val app: Application) : InjektModule { @@ -52,7 +56,5 @@ class AppModule(val app: Application) : InjektModule { GlobalScope.launch { get() } GlobalScope.launch { get() } - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 25b91118ca..6bb2fca7ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.updater.UpdaterJob +import eu.kanade.tachiyomi.ui.library.LibraryPresenter import java.io.File object Migrations { @@ -25,7 +26,7 @@ object Migrations { if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) { UpdaterJob.setupTask() } - return false + return BuildConfig.DEBUG } if (oldVersion < 14) { @@ -63,9 +64,10 @@ object Migrations { } if (oldVersion < 54) DownloadProvider(context).renameChaapters() + if (oldVersion < 62) + LibraryPresenter.updateDB() return true } return false } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt index 643c469a28..8a8cc45fba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.backup import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID - object BackupConst { const val INTENT_FILTER = "SettingsBackupFragment" @@ -18,5 +17,5 @@ object BackupConst { const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME" const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH" const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE" - const val EXTRA_MINI_ERROR= "$ID.$INTENT_FILTER.EXTRA_MINI_ERROR" -} \ No newline at end of file + const val EXTRA_MINI_ERROR = "$ID.$INTENT_FILTER.EXTRA_MINI_ERROR" +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt index d7095642db..1721ff624c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt @@ -45,7 +45,6 @@ class BackupCreateService : IntentService(NAME) { } context.startService(intent) } - } private val backupManager by lazy { BackupManager(this) } @@ -60,5 +59,4 @@ class BackupCreateService : IntentService(NAME) { if (uri != null) backupManager.createBackup(uri, flags, false) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index fb03033dc6..69bf1a57f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -3,8 +3,15 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.content.Intent import android.net.Uri -import com.github.salomonbrys.kotson.* -import com.google.gson.* +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.registerTypeAdapter +import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter +import com.github.salomonbrys.kotson.set +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK @@ -22,18 +29,31 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.DHistory -import eu.kanade.tachiyomi.data.backup.serializer.* +import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter +import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter +import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter +import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter +import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.* +import eu.kanade.tachiyomi.data.database.models.CategoryImpl +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.ChapterImpl +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.system.sendLocalBroadcast +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import rx.Observable import timber.log.Timber import uy.kohesive.injekt.injectLazy @@ -267,7 +287,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { */ suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga { return withContext(Dispatchers.IO) { - val networkManga = source.fetchMangaDetails(manga).toBlocking().single() + val networkManga = source.fetchMangaDetailsAsync(manga)!! manga.copyFrom(networkManga) manga.favorite = true manga.initialized = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index 9417af1fb0..af8a4b7c99 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -25,19 +25,23 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.* +import eu.kanade.tachiyomi.data.database.models.ChapterImpl +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceNotFoundException import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.storage.getUriCompat +import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.notificationManager import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import eu.kanade.tachiyomi.util.system.isServiceRunning import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File @@ -50,7 +54,6 @@ import java.util.concurrent.TimeUnit */ class BackupRestoreService : Service() { - /** * Wake lock that will be held until the service is destroyed. */ @@ -83,7 +86,6 @@ class BackupRestoreService : Service() { */ private val trackingErrors = mutableListOf() - /** * List containing missing sources */ @@ -109,7 +111,6 @@ class BackupRestoreService : Service() { */ internal val trackManager: TrackManager by injectLazy() - /** * Method called when the service is created. It injects dependencies and acquire the wake lock. */ @@ -147,9 +148,7 @@ class BackupRestoreService : Service() { * @return the start value of the command. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent == null) return START_NOT_STICKY - - val uri = intent.getParcelableExtra(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY + val uri = intent?.getParcelableExtra(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY // Unsubscribe from any previous subscription if needed. job?.cancel() @@ -159,7 +158,7 @@ class BackupRestoreService : Service() { stopSelf(startId) } job = GlobalScope.launch(handler) { - restoreBackup(uri!!) + restoreBackup(uri) } job?.invokeOnCompletion { stopSelf(startId) } @@ -179,7 +178,7 @@ class BackupRestoreService : Service() { */ private suspend fun restoreBackup(uri: Uri) { val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) - val json = JsonParser().parse(reader).asJsonObject + val json = JsonParser.parseReader(reader).asJsonObject // Get parser version val version = json.get(VERSION)?.asInt ?: 1 @@ -214,7 +213,6 @@ class BackupRestoreService : Service() { showResultNotification(logFile.parent, logFile.name) } - /**Restore categories if they were backed up * */ @@ -244,8 +242,7 @@ class BackupRestoreService : Service() { if (job?.isCancelled == false) { showProgressNotification(restoreProgress, totalAmount, manga.title) restoreProgress += 1 - } - else { + } else { throw java.lang.Exception("Job was cancelled") } val dbManga = backupManager.getMangaFromDatabase(manga) @@ -260,7 +257,7 @@ class BackupRestoreService : Service() { } if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) { - //manga gets chapters added + // manga gets chapters added backupManager.restoreChapterFetch(source, manga, chapters) } // Restore categories @@ -278,8 +275,7 @@ class BackupRestoreService : Service() { val cause = e.cause if (cause is SourceNotFoundException) { sourcesMissing.add(cause.id) - } - else if (e.message?.contains("licensed", true) == true) { + } else if (e.message?.contains("licensed", true) == true) { lincensedManga++ } errors.add("${manga.title} - ${cause?.message ?: e.message}") @@ -294,19 +290,19 @@ class BackupRestoreService : Service() { * @param manga manga that needs updating. * @param tracks list containing tracks from restore file. */ - private fun trackingFetch(manga: Manga, tracks: List) { + private suspend fun trackingFetch(manga: Manga, tracks: List) { tracks.forEach { track -> val service = trackManager.getService(track.sync_id) if (service != null && service.isLogged) { - service.refresh(track) - .doOnNext { db.insertTrack(it).executeAsBlocking() } - .onErrorReturn { - errors.add("${manga.title} - ${it.message}") - track - } + try { + service.refresh(track) + db.insertTrack(track).executeAsBlocking() + } catch (e: Exception) { + errors.add("${manga.title} - ${e.message}") + } } else { errors.add("${manga.title} - ${service?.name} not logged in") - val notLoggedIn = getString(R.string.not_logged_into, service?.name) + val notLoggedIn = getString(R.string.not_logged_into_, service?.name) trackingErrors.add(notLoggedIn) } } @@ -355,7 +351,6 @@ class BackupRestoreService : Service() { NotificationReceiver.cancelRestorePendingBroadcast(this) } - /** * Shows the notification containing the currently updating manga and the progress. * @@ -366,7 +361,7 @@ class BackupRestoreService : Service() { private fun showProgressNotification(current: Int, total: Int, title: String) { notificationManager.notify(Notifications.ID_RESTORE_PROGRESS, progressNotification .setContentTitle(title.chop(30)) - .setContentText(getString(R.string.backup_restoring_progress, restoreProgress, + .setContentText(getString(R.string.restoring_progress, restoreProgress, totalAmount)) .setProgress(total, current, false) .build()) @@ -392,7 +387,7 @@ class BackupRestoreService : Service() { content.add(trackingErrorsString) } if (cancelled > 0) - content.add(getString(R.string.restore_completed_content_2, cancelled)) + content.add(getString(R.string.restore_content_skipped, cancelled)) val restoreString = content.joinToString("\n") @@ -405,7 +400,7 @@ class BackupRestoreService : Service() { .setColor(ContextCompat.getColor(this, R.color.colorAccent)) if (errors.size > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) { resultNotification.addAction(R.drawable.ic_clear_grey_24dp_img, getString(R.string - .notification_action_error_log), getErrorLogIntent(path, file)) + .view_all_errors), getErrorLogIntent(path, file)) } notificationManager.notify(Notifications.ID_RESTORE_COMPLETE, resultNotification.build()) } @@ -471,4 +466,4 @@ class BackupRestoreService : Service() { context.stopService(Intent(context, BackupRestoreService::class.java)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index dd50553c0d..d0e46e1b3a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -1,7 +1,8 @@ package eu.kanade.tachiyomi.data.backup.models import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale /** * Json values @@ -20,4 +21,4 @@ object Backup { val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) return "tachiyomi_$date.json" } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/DHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/DHistory.kt index 3623dd0d37..a5e1c1a0f3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/DHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/DHistory.kt @@ -1,3 +1,3 @@ package eu.kanade.tachiyomi.data.backup.models -data class DHistory(val url: String,val lastRead: Long) \ No newline at end of file +data class DHistory(val url: String, val lastRead: Long) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/CategoryTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/CategoryTypeAdapter.kt index b31279268f..1beb5d9798 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/CategoryTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/CategoryTypeAdapter.kt @@ -28,4 +28,4 @@ object CategoryTypeAdapter { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/HistoryTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/HistoryTypeAdapter.kt index e313c3b90b..863a1a1f30 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/HistoryTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/HistoryTypeAdapter.kt @@ -29,4 +29,4 @@ object HistoryTypeAdapter { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt index e1f7634916..1192f39b4a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.serializer import com.github.salomonbrys.kotson.typeAdapter import com.google.gson.TypeAdapter import eu.kanade.tachiyomi.data.database.models.MangaImpl +import kotlin.math.max /** * JSON Serializer used to write / read [MangaImpl] to / from json @@ -14,9 +15,9 @@ object MangaTypeAdapter { write { beginArray() value(it.url) - value(it.originalTitle()) + value(it.title) value(it.source) - value(it.viewer) + value(max(0, it.viewer)) value(it.chapter_flags) endArray() } @@ -34,4 +35,4 @@ object MangaTypeAdapter { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt index 0cc16d38e9..de78b8c115 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.backup.serializer -import android.telecom.DisconnectCause.REMOTE import com.github.salomonbrys.kotson.typeAdapter import com.google.gson.TypeAdapter import com.google.gson.stream.JsonToken diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index 6f5f23958b..b20b284655 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.saveTo import okhttp3.Response -import okio.Okio import okio.buffer import okio.sink import rx.Observable @@ -136,7 +135,6 @@ class ChapterCache(private val context: Context) { diskCache.flush() editor.commit() editor.abortUnlessCommitted() - } catch (e: Exception) { // Ignore. } finally { @@ -202,4 +200,3 @@ class ChapterCache(private val context: Context) { return "${chapter.manga_id}${chapter.url}" } } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index 479ad00c24..2fa227146a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -20,8 +20,8 @@ class CoverCache(private val context: Context) { /** * Cache directory used for cache management. */ - private val cacheDir = context.getExternalFilesDir("covers") ?: - File(context.filesDir, "covers").also { it.mkdirs() } + private val cacheDir = context.getExternalFilesDir("covers") + ?: File(context.filesDir, "covers").also { it.mkdirs() } /** * Returns the cover from cache. @@ -37,7 +37,7 @@ class CoverCache(private val context: Context) { * Copy the given stream to this cache. * * @param thumbnailUrl url of the thumbnail. - * @param inputStream the stream to copy. + * @param inputStream the stream to copy. * @throws IOException if there's any error. */ @Throws(IOException::class) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 815a6baee0..4509319c9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -29,8 +29,8 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory /** * This class provides operations to manage the database through its interfaces. */ -open class DatabaseHelper(context: Context) -: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, +open class DatabaseHelper(context: Context) : +MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, SearchMetadataQueries { private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) @@ -52,5 +52,4 @@ open class DatabaseHelper(context: Context) inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) fun lowLevel() = db.lowLevel() - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt index 252ac08290..caaba0e101 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt @@ -22,4 +22,3 @@ inline fun StorIOSQLite.inTransactionReturn(block: () -> T): T { lowLevel().endTransaction() } } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index bba9db5068..7fbd7089a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.data.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import eu.kanade.tachiyomi.data.database.tables.* +import eu.kanade.tachiyomi.data.database.tables.CategoryTable +import eu.kanade.tachiyomi.data.database.tables.ChapterTable +import eu.kanade.tachiyomi.data.database.tables.HistoryTable +import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable +import eu.kanade.tachiyomi.data.database.tables.MangaTable +import eu.kanade.tachiyomi.data.database.tables.TrackTable class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { @@ -18,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { /** * Version of the database. */ - const val DATABASE_VERSION = 10 + const val DATABASE_VERSION = 12 } override fun onCreate(db: SupportSQLiteDatabase) = with(db) { @@ -73,10 +75,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { if (oldVersion < 10) { db.execSQL(CategoryTable.addMangaOrder) } + if (oldVersion < 11) { + db.execSQL(ChapterTable.pagesLeftQuery) + } + if (oldVersion < 12) { + db.execSQL(MangaTable.addDateAddedCol) + } } override fun onConfigure(db: SupportSQLiteDatabase) { db.setForeignKeyConstraintsEnabled(true) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt index 7af8dff0c1..4609852b96 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt @@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite interface DbProvider { val db: DefaultStorIOSQLite - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/CategoryTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/CategoryTypeMapping.kt index 718a42f4b0..beefde472b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/CategoryTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/CategoryTypeMapping.kt @@ -47,7 +47,6 @@ class CategoryPutResolver : DefaultPutResolver() { val orderString = obj.mangaOrder.joinToString("/") put(COL_MANGA_ORDER, orderString) } - } } @@ -60,12 +59,17 @@ class CategoryGetResolver : DefaultGetResolver() { flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS)) val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER)) - if (orderString?.firstOrNull()?.isLetter() == true) { - mangaSort = orderString.first() - mangaOrder = emptyList() + when { + orderString.isNullOrBlank() -> { + mangaSort = 'a' + mangaOrder = emptyList() + } + orderString.firstOrNull()?.isLetter() == true -> { + mangaSort = orderString.first() + mangaOrder = emptyList() + } + else -> mangaOrder = orderString.split("/")?.mapNotNull { it.toLongOrNull() } } - else - mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() } ?: emptyList() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt index 2e903a64ba..c9d9b13d3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME +import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_PAGES_LEFT import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER @@ -54,6 +55,7 @@ class ChapterPutResolver : DefaultPutResolver() { put(COL_DATE_FETCH, obj.date_fetch) put(COL_DATE_UPLOAD, obj.date_upload) put(COL_LAST_PAGE_READ, obj.last_page_read) + put(COL_PAGES_LEFT, obj.pages_left) put(COL_CHAPTER_NUMBER, obj.chapter_number) put(COL_SOURCE_ORDER, obj.source_order) } @@ -72,6 +74,7 @@ class ChapterGetResolver : DefaultGetResolver() { date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH)) date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD)) last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ)) + pages_left = cursor.getInt(cursor.getColumnIndex(COL_PAGES_LEFT)) chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER)) source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER)) } @@ -85,4 +88,3 @@ class ChapterDeleteResolver : DefaultDeleteResolver() { .whereArgs(obj.id) .build() } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index ac89bbc209..73dfa7f0f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt @@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl 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_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 @@ -64,6 +65,7 @@ class MangaPutResolver : DefaultPutResolver() { put(COL_VIEWER, obj.viewer) put(COL_HIDE_TITLE, obj.hide_title) put(COL_CHAPTER_FLAGS, obj.chapter_flags) + put(COL_DATE_ADDED, obj.date_added) } } @@ -85,6 +87,7 @@ interface BaseMangaGetResolver { viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)) == 1 + date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/SearchMetadataTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/SearchMetadataTypeMapping.kt index 25df012756..1f9248a7c1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/SearchMetadataTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/SearchMetadataTypeMapping.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.database.mappers - import android.content.ContentValues import android.database.Cursor import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping @@ -63,4 +62,4 @@ class SearchMetadataDeleteResolver : DefaultDeleteResolver() { .where("$COL_MANGA_ID = ?") .whereArgs(obj.mangaId) .build() -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt index 6759316de9..797c6a2524 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt @@ -54,7 +54,6 @@ class TrackPutResolver : DefaultPutResolver() { put(COL_STATUS, obj.status) put(COL_TRACKING_URL, obj.tracking_url) put(COL_SCORE, obj.score) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt index ecf8988560..632ce67bf7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.models import android.content.Context import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.library.LibrarySort import java.io.Serializable interface Category : Serializable { @@ -14,36 +15,107 @@ interface Category : Serializable { var flags: Int - var mangaOrder:List + var mangaOrder: List - var mangaSort:Char? + var mangaSort: Char? + + var isFirst: Boolean? + var isLast: Boolean? val nameLower: String get() = name.toLowerCase() fun isAscending(): Boolean { - return ((mangaSort?.minus('a') ?: 0) % 2) != 1 + return ((mangaSort?.minus('a') ?: 0) % 2) != 1 } - companion object { - const val ALPHA_ASC = 'a' - const val ALPHA_DSC = 'b' - const val UPDATED_ASC = 'c' - const val UPDATED_DSC = 'd' - const val UNREAD_ASC = 'e' - const val UNREAD_DSC = 'f' - const val LAST_READ_ASC = 'g' - const val LAST_READ_DSC = 'h' + fun sortingMode(): Int? = when (mangaSort) { + ALPHA_ASC, ALPHA_DSC -> LibrarySort.ALPHA + UPDATED_ASC, UPDATED_DSC -> LibrarySort.LATEST_CHAPTER + UNREAD_ASC, UNREAD_DSC -> LibrarySort.UNREAD + LAST_READ_ASC, LAST_READ_DSC -> LibrarySort.LAST_READ + TOTAL_ASC, TOTAL_DSC -> LibrarySort.TOTAL + DRAG_AND_DROP -> LibrarySort.DRAG_AND_DROP + DATE_ADDED_ASC, DATE_ADDED_DSC -> LibrarySort.DATE_ADDED + else -> null + } + fun sortRes(): Int = when (mangaSort) { + ALPHA_ASC, ALPHA_DSC -> R.string.title + UPDATED_ASC, UPDATED_DSC -> R.string.latest_chapter + UNREAD_ASC, UNREAD_DSC -> R.string.unread + LAST_READ_ASC, LAST_READ_DSC -> R.string.last_read + TOTAL_ASC, TOTAL_DSC -> R.string.total_chapters + DATE_ADDED_ASC, DATE_ADDED_DSC -> R.string.date_added + else -> R.string.drag_and_drop + } + + fun catSortingMode(): Int? = when (mangaSort) { + ALPHA_ASC, ALPHA_DSC -> 0 + UPDATED_ASC, UPDATED_DSC -> 1 + UNREAD_ASC, UNREAD_DSC -> 2 + LAST_READ_ASC, LAST_READ_DSC -> 3 + TOTAL_ASC, TOTAL_DSC -> 4 + DATE_ADDED_ASC, DATE_ADDED_DSC -> 5 + else -> null + } + + fun changeSortTo(sort: Int) { + mangaSort = when (sort) { + LibrarySort.ALPHA -> ALPHA_ASC + LibrarySort.LATEST_CHAPTER -> UPDATED_ASC + LibrarySort.UNREAD -> UNREAD_ASC + LibrarySort.LAST_READ -> LAST_READ_ASC + LibrarySort.TOTAL -> ALPHA_ASC + LibrarySort.DATE_ADDED -> DATE_ADDED_ASC + else -> ALPHA_ASC + } + } + + companion object { + private const val DRAG_AND_DROP = 'D' + private const val ALPHA_ASC = 'a' + private const val ALPHA_DSC = 'b' + private const val UPDATED_ASC = 'c' + private const val UPDATED_DSC = 'd' + private const val UNREAD_ASC = 'e' + private const val UNREAD_DSC = 'f' + private const val LAST_READ_ASC = 'g' + private const val LAST_READ_DSC = 'h' + private const val TOTAL_ASC = 'i' + private const val TOTAL_DSC = 'j' + private const val DATE_ADDED_ASC = 'k' + private const val DATE_ADDED_DSC = 'l' fun create(name: String): Category = CategoryImpl().apply { this.name = name } - fun createDefault(context: Context): Category = create(context.getString(R.string.default_columns)) - .apply { - id = - 0 } - } + fun createDefault(context: Context): Category = + create(context.getString(R.string.default_value)).apply { + id = 0 + isFirst = true + } -} \ No newline at end of file + fun createAll(context: Context, libSort: Int, ascending: Boolean): Category = + create(context.getString(R.string.all)).apply { + id = -1 + mangaSort = when (libSort) { + LibrarySort.ALPHA -> ALPHA_ASC + LibrarySort.LATEST_CHAPTER -> UPDATED_ASC + LibrarySort.UNREAD -> UNREAD_ASC + LibrarySort.LAST_READ -> LAST_READ_ASC + LibrarySort.TOTAL -> TOTAL_ASC + LibrarySort.DATE_ADDED -> DATE_ADDED_ASC + LibrarySort.DRAG_AND_DROP -> DRAG_AND_DROP + else -> DRAG_AND_DROP + } + if (mangaSort != DRAG_AND_DROP && !ascending) { + mangaSort?.plus(1) + } + order = -1 + isFirst = true + isLast = true + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt index 7374effc3d..1332434efe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt @@ -14,6 +14,10 @@ class CategoryImpl : Category { override var mangaSort: Char? = null + override var isFirst: Boolean? = null + + override var isLast: Boolean? = null + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false @@ -26,5 +30,4 @@ class CategoryImpl : Category { override fun hashCode(): Int { return name.hashCode() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index 589ed671db..118344ddb7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -15,6 +15,8 @@ interface Chapter : SChapter, Serializable { var last_page_read: Int + var pages_left: Int + var date_fetch: Long var source_order: Int diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index d2067abfd1..0daae16f5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -18,6 +18,8 @@ class ChapterImpl : Chapter { override var last_page_read: Int = 0 + override var pages_left: Int = 0 + override var date_fetch: Long = 0 override var date_upload: Long = 0 @@ -37,5 +39,4 @@ class ChapterImpl : Chapter { override fun hashCode(): Int { return url.hashCode() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt index 30f50972c5..dff3bcb155 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt @@ -35,7 +35,7 @@ interface History : Serializable { * @param chapter chapter object * @return history object */ - fun create(chapter: Chapter): History = HistoryImpl().apply { + fun create(chapter: Chapter): History = HistoryImpl().apply { this.chapter_id = chapter.id!! } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt index b9a7d94286..31523a7b51 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt @@ -6,4 +6,13 @@ class LibraryManga : MangaImpl() { var category: Int = 0 -} \ No newline at end of file + fun isBlank() = id == Long.MIN_VALUE + + companion object { + fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply { + title = "" + id = Long.MIN_VALUE + category = categoryId + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index c7dff69dfa..947622ba9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -1,6 +1,13 @@ package eu.kanade.tachiyomi.data.database.models +import android.content.Context +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Locale interface Manga : SManga { @@ -12,6 +19,8 @@ interface Manga : SManga { var last_update: Long + var date_added: Long + var viewer: Int var chapter_flags: Int @@ -20,14 +29,108 @@ interface Manga : SManga { fun setChapterOrder(order: Int) { setFlags(order, SORT_MASK) + setFlags(SORT_LOCAL, SORT_SELF_MASK) } + fun setSortToGlobal() = setFlags(SORT_GLOBAL, SORT_SELF_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 + fun sortDescending(): Boolean = chapter_flags and SORT_MASK == SORT_DESC + + fun usesLocalSort(): Boolean = chapter_flags and SORT_SELF_MASK == SORT_LOCAL + + fun sortDescending(defaultDesc: Boolean): Boolean { + return if (chapter_flags and SORT_SELF_MASK == SORT_GLOBAL) defaultDesc + else sortDescending() + } + + fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and DISPLAY_MASK == DISPLAY_NUMBER + + fun mangaType(context: Context): String { + return context.getString(when (mangaType()) { + TYPE_WEBTOON -> R.string.webtoon + TYPE_MANHWA -> R.string.manhwa + TYPE_MANHUA -> R.string.manhua + TYPE_COMIC -> R.string.comic + else -> R.string.manga + }).toLowerCase(Locale.getDefault()) + } + + /** + * The type of comic the manga is (ie. manga, manhwa, manhua) + */ + fun mangaType(): Int { + val sourceName = Injekt.get().getOrStub(source).name + val currentTags = genre?.split(",")?.map { it.trim().toLowerCase(Locale.US) } + return if (currentTags?.any + { tag -> + tag.startsWith("japanese") || tag == "manga" + } == true) + TYPE_MANGA + else if (currentTags?.any + { tag -> + tag.startsWith("english") || tag == "comic" + } == true || isComicSource(sourceName)) + TYPE_COMIC + else if (currentTags?.any + { tag -> + tag.startsWith("chinese") || tag == "manhua" + } == true || + sourceName.contains("manhua", true)) + TYPE_MANHUA + else if (currentTags?.any + { tag -> + tag == "long strip" || tag == "manhwa" + } == true || isWebtoonSource(sourceName)) + TYPE_MANHWA + else if (currentTags?.any + { tag -> + tag.startsWith("webtoon") + } == true) + TYPE_WEBTOON + else TYPE_MANGA + } + + /** + * The type the reader should use. Different from manga type as certain manga has different + * read types + */ + fun defaultReaderType(): Int { + val sourceName = Injekt.get().getOrStub(source).name + val currentTags = genre?.split(",")?.map { it.trim().toLowerCase(Locale.US) } + return if (currentTags?.any + { tag -> + tag == "long strip" || tag == "manhwa" || + tag.contains("webtoon") + } == true || isWebtoonSource(sourceName) || + sourceName.contains("tapastic", true)) + ReaderActivity.WEBTOON + else if (currentTags?.any + { tag -> + tag.startsWith("chinese") || tag == "manhua" || + tag.startsWith("english") || tag == "comic" + } == true || isComicSource(sourceName) || + sourceName.contains("manhua", true)) + ReaderActivity.LEFT_TO_RIGHT + else 0 + } + + fun isWebtoonSource(sourceName: String): Boolean { + return sourceName.contains("webtoon", true) || + sourceName.contains("manwha", true) || + sourceName.contains("toonily", true) + } + + fun isComicSource(sourceName: String): Boolean { + return sourceName.contains("gunnerkrigg", true) || + sourceName.contains("gunnerkrigg", true) || + sourceName.contains("dilbert", true) || + sourceName.contains("cyanide", true) || + sourceName.contains("xkcd", true) || + sourceName.contains("tapastic", true) } // Used to display the chapter's title one way or another @@ -57,6 +160,10 @@ interface Manga : SManga { const val SORT_ASC = 0x00000001 const val SORT_MASK = 0x00000001 + const val SORT_GLOBAL = 0x00000000 + const val SORT_LOCAL = 0x00001000 + const val SORT_SELF_MASK = 0x00001000 + // Generic filter that does not filter anything const val SHOW_ALL = 0x00000000 @@ -80,6 +187,12 @@ interface Manga : SManga { const val DISPLAY_NUMBER = 0x00100000 const val DISPLAY_MASK = 0x00100000 + const val TYPE_MANGA = 0 + const val TYPE_MANHWA = 1 + const val TYPE_MANHUA = 2 + const val TYPE_COMIC = 3 + const val TYPE_WEBTOON = 4 + fun create(source: Long): Manga = MangaImpl().apply { this.source = source } @@ -90,5 +203,4 @@ interface Manga : SManga { this.source = source } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt index 305d5ef9cf..2203370884 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt @@ -17,5 +17,4 @@ class MangaCategory { return mc } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index e11fe8f832..224f6c53db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -5,6 +5,10 @@ package eu.kanade.tachiyomi.data.database.models * * @param manga object containing manga * @param chapter object containing chater - * @param history object containing history + * @param history object containing history */ -data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) +data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) { + companion object { + fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index 8f5313a42a..82dfcefd36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -4,8 +4,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.source.model.SManga import uy.kohesive.injekt.injectLazy -import kotlin.collections.MutableMap -import kotlin.collections.mutableMapOf import kotlin.collections.set open class MangaImpl : Manga { @@ -36,22 +34,20 @@ open class MangaImpl : Manga { override var initialized: Boolean = false - override var viewer: Int = 0 + override var viewer: Int = -1 override var chapter_flags: Int = 0 override var hide_title: Boolean = false + override var date_added: Long = 0 + override fun copyFrom(other: SManga) { if (other is MangaImpl && (other as MangaImpl)::title.isInitialized && - !other.title.isBlank() && other.title != originalTitle()) { - val oldTitle = originalTitle() - title = if (currentTitle() != originalTitle()) { - val customTitle = currentTitle() - val trueTitle = other.title - "${customTitle}${SManga.splitter}${trueTitle}" - } else other.title - val db:DownloadManager by injectLazy() + !other.title.isBlank() && other.title != title) { + val oldTitle = title + title = other.title + val db: DownloadManager by injectLazy() val provider = DownloadProvider(db.context) provider.renameMangaFolder(oldTitle, title, source) } @@ -65,7 +61,6 @@ open class MangaImpl : Manga { val manga = other as Manga return url == manga.url - } override fun hashCode(): Int { @@ -73,7 +68,7 @@ open class MangaImpl : Manga { } companion object { - private var lastCoverFetch:HashMap = hashMapOf() + private var lastCoverFetch: HashMap = hashMapOf() fun setLastCoverFetch(id: Long, time: Long) { lastCoverFetch[id] = time @@ -81,5 +76,4 @@ open class MangaImpl : Manga { fun getLastCoverFetch(id: Long) = lastCoverFetch[id] ?: 0 } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SearchMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SearchMetadata.kt index 64b1476378..d6bc94fe33 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SearchMetadata.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SearchMetadata.kt @@ -18,4 +18,4 @@ data class SearchMetadata( ) { // Transient information attached to this piece of metadata, useful for caching var transientCache: Map? = null -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index 19133e0371..c64363efa0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -37,5 +37,4 @@ interface Track : Serializable { sync_id = serviceId } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index 65f6ec7ab1..03a878e146 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -41,5 +41,4 @@ class TrackImpl : Track { result = 31 * result + media_id return result } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt index edf839d8fa..d677d880d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt @@ -32,5 +32,4 @@ interface CategoryQueries : DbProvider { fun deleteCategory(category: Category) = db.delete().`object`(category).prepare() fun deleteCategories(categories: List) = db.delete().objects(categories).prepare() - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index c10d0b9367..413242d021 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -6,12 +6,14 @@ import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaChapter +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver +import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver import eu.kanade.tachiyomi.data.database.tables.ChapterTable -import java.util.* +import java.util.Date interface ChapterQueries : DbProvider { @@ -34,6 +36,16 @@ interface ChapterQueries : DbProvider { .withGetResolver(MangaChapterGetResolver.INSTANCE) .prepare() + fun getUpdatedManga(date: Date, search: String = "", endless: Boolean) = db.get() + .listOfObjects(MangaChapterHistory::class.java) + .withQuery(RawQuery.builder() + .query(getRecentsQueryDistinct(search, endless)) + .args(date.time) + .observesTables(ChapterTable.TABLE) + .build()) + .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) + .prepare() + fun getChapter(id: Long) = db.get() .`object`(Chapter::class.java) .withQuery(Query.builder() @@ -88,5 +100,4 @@ interface ChapterQueries : DbProvider { .objects(chapters) .withPutResolver(ChapterSourceOrderPutResolver()) .prepare() - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt index bb361af48c..f835d79f60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt @@ -8,7 +8,8 @@ import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver import eu.kanade.tachiyomi.data.database.tables.HistoryTable -import java.util.* +import eu.kanade.tachiyomi.data.database.tables.MangaTable +import java.util.Date interface HistoryQueries : DbProvider { @@ -33,6 +34,21 @@ interface HistoryQueries : DbProvider { .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .prepare() + /** + * Returns history of recent manga containing last read chapter in 25s + * @param date recent date range + * @offset offset the db by + */ + fun getRecentlyAdded(date: Date, search: String = "", endless: Boolean) = db.get() + .listOfObjects(MangaChapterHistory::class.java) + .withQuery(RawQuery.builder() + .query(getRecentAdditionsQuery(search, endless)) + .args(date.time) + .observesTables(MangaTable.TABLE) + .build()) + .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) + .prepare() + /** * Returns history of recent manga containing last read chapter in 25s * @param date recent date range @@ -48,6 +64,21 @@ interface HistoryQueries : DbProvider { .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .prepare() + /** + * Returns history of recent manga containing last read chapter in 25s + * @param date recent date range + * @offset offset the db by + */ + fun getRecentsWithUnread(date: Date, search: String = "", endless: Boolean) = db.get() + .listOfObjects(MangaChapterHistory::class.java) + .withQuery(RawQuery.builder() + .query(getRecentReadWithUnreadChapters(search, endless)) + .args(date.time) + .observesTables(HistoryTable.TABLE) + .build()) + .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) + .prepare() + fun getHistoryByMangaId(mangaId: Long) = db.get() .listOfObjects(History::class.java) .withQuery(RawQuery.builder() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt index 7ad7f937e2..926339d0c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaCategoryQueries.kt @@ -28,5 +28,4 @@ interface MangaCategoryQueries : DbProvider { insertMangasCategories(mangasCategories).executeAsBlocking() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 22d21cafc4..eac34db9bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -6,7 +6,14 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.resolvers.* +import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver +import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver +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 import eu.kanade.tachiyomi.data.database.tables.CategoryTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable @@ -30,6 +37,15 @@ interface MangaQueries : DbProvider { .withGetResolver(LibraryMangaGetResolver.INSTANCE) .prepare() + fun getLibraryManga(id: Long) = db.get() + .`object`(LibraryManga::class.java) + .withQuery(RawQuery.builder() + .query(getLibraryMangaQuery(id)) + .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) + .build()) + .withGetResolver(LibraryMangaGetResolver.INSTANCE) + .prepare() + fun getFavoriteMangas() = db.get() .listOfObjects(Manga::class.java) .withQuery(Query.builder() @@ -77,16 +93,16 @@ interface MangaQueries : DbProvider { .withPutResolver(MangaFavoritePutResolver()) .prepare() + fun updateMangaAdded(manga: Manga) = db.put() + .`object`(manga) + .withPutResolver(MangaDateAddedPutResolver()) + .prepare() + fun updateMangaViewer(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaViewerPutResolver()) .prepare() - fun updateMangaHideTitle(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaHideTitlePutResolver()) - .prepare() - fun updateMangaTitle(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaTitlePutResolver()) @@ -129,5 +145,5 @@ interface MangaQueries : DbProvider { .prepare() fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java) - .withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare(); + .withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 1c532948f3..d2a1bf6b26 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -30,14 +30,73 @@ val libraryQuery = """ ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} """ +fun getLibraryMangaQuery(id: Long) = """ + SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} + FROM ( + SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} + FROM ${Manga.TABLE} + LEFT JOIN ( + SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread + FROM ${Chapter.TABLE} + WHERE ${Chapter.COL_READ} = 0 + GROUP BY ${Chapter.COL_MANGA_ID} + ) AS C + ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} + WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_ID} = $id + GROUP BY ${Manga.COL_ID} + ORDER BY ${Manga.COL_TITLE} + ) AS M + LEFT JOIN ( + SELECT * FROM ${MangaCategory.TABLE}) AS MC + ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} +""" + /** * Query to get the recent chapters of manga from the library up to a date. */ 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 +""" + +/** + * Query to get the recently added manga + */ +fun getRecentAdditionsQuery(search: String, endless: Boolean) = """ + SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} + WHERE ${Manga.COL_FAVORITE} = 1 + AND ${Manga.COL_DATE_ADDED} > ? + AND lower(${Manga.COL_TITLE}) LIKE '%$search%' + ORDER BY ${Manga.COL_DATE_ADDED} DESC + ${if (endless) "" else "LIMIT 8"} +""" + +/** + * Query to get the manga with recently uploaded chapters + */ +fun getRecentsQueryDistinct(search: String, endless: Boolean) = """ + SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.* + FROM ${Manga.TABLE} + JOIN ${Chapter.TABLE} + ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} + JOIN ( + SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID},MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) + FROM ${Chapter.TABLE} JOIN ${Manga.TABLE} + ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} + WHERE ${Chapter.COL_DATE_UPLOAD} > ? + AND ${Chapter.COL_READ} = 0 + GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS newest_chapter + ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = newest_chapter.${Chapter.COL_MANGA_ID} + WHERE ${Manga.COL_FAVORITE} = 1 + AND newest_chapter.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} + AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED} + AND lower(${Manga.COL_TITLE}) LIKE '%$search%' ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC + ${if (endless) "" else "LIMIT 8"} """ /** @@ -62,7 +121,7 @@ fun getRecentMangasQuery(offset: Int = 0, search: String = "") = """ ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID} WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} - AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%${search}%' + AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%' ORDER BY max_last_read.${History.COL_LAST_READ} DESC LIMIT 25 OFFSET $offset """ @@ -88,11 +147,51 @@ fun getRecentMangasLimitQuery(limit: Int = 25, search: String = "") = """ ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID} WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} - AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%${search}%' + AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%' ORDER BY max_last_read.${History.COL_LAST_READ} DESC LIMIT $limit """ +/** + * Query to get the recently read manga that has more chapters to read + * The first from checks that there's an unread chapter + * The max_last_read table contains the most recent chapters grouped by manga + * The select statement returns all information of chapters that have the same id as the chapter in max_last_read + * and are read after the given time period + */ +fun getRecentReadWithUnreadChapters(search: String = "", endless: Boolean) = """ + SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* + FROM ( + SELECT ${Manga.TABLE}.* + FROM ${Manga.TABLE} + LEFT JOIN ( + SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread + FROM ${Chapter.TABLE} + WHERE ${Chapter.COL_READ} = 0 + GROUP BY ${Chapter.COL_MANGA_ID} + ) AS C + ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} + WHERE C.unread > 0 + GROUP BY ${Manga.COL_ID} + ORDER BY ${Manga.COL_TITLE} + ) AS ${Manga.TABLE} + JOIN ${Chapter.TABLE} + ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} + JOIN ${History.TABLE} + ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} + JOIN ( + SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ} + FROM ${Chapter.TABLE} JOIN ${History.TABLE} + ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} + GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read + ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID} + WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? + AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} + AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%' + ORDER BY max_last_read.${History.COL_LAST_READ} DESC + ${if (endless) "" else "LIMIT 8"} +""" + fun getHistoryByMangaId() = """ SELECT ${History.TABLE}.* FROM ${History.TABLE} @@ -121,7 +220,7 @@ fun getLastReadMangaQuery() = """ ORDER BY max DESC """ -fun getTotalChapterMangaQuery()= """ +fun getTotalChapterMangaQuery() = """ SELECT ${Manga.TABLE}.* FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} @@ -138,4 +237,4 @@ fun getCategoriesForMangaQuery() = """ JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} = ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} WHERE ${MangaCategory.COL_MANGA_ID} = ? -""" \ No newline at end of file +""" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/SearchMetadataQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/SearchMetadataQueries.kt index e6b67ea057..c23c25466d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/SearchMetadataQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/SearchMetadataQueries.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.database.queries - import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.Query import eu.kanade.tachiyomi.data.database.DbProvider @@ -42,4 +41,4 @@ interface SearchMetadataQueries : DbProvider { .table(SearchMetadataTable.TABLE) .build()) .prepare() -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt index a93877faf7..fee55dbd54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt @@ -30,5 +30,4 @@ interface TrackQueries : DbProvider { .whereArgs(manga.id, sync.id) .build()) .prepare() - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterBackupPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterBackupPutResolver.kt index 1c3e6fb74e..d42758beae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterBackupPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterBackupPutResolver.kt @@ -30,6 +30,4 @@ class ChapterBackupPutResolver : PutResolver() { put(ChapterTable.COL_BOOKMARK, chapter.bookmark) put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read) } - } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt index 18009d711e..e2304eaaf1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterProgressPutResolver.kt @@ -29,7 +29,6 @@ class ChapterProgressPutResolver : PutResolver() { put(ChapterTable.COL_READ, chapter.read) put(ChapterTable.COL_BOOKMARK, chapter.bookmark) put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read) + put(ChapterTable.COL_PAGES_LEFT, chapter.pages_left) } - } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterSourceOrderPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterSourceOrderPutResolver.kt index 77bc0afadc..40c7a7df79 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterSourceOrderPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/ChapterSourceOrderPutResolver.kt @@ -28,5 +28,4 @@ class ChapterSourceOrderPutResolver : PutResolver() { fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply { put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt index f1d68c22a9..98ec17cb4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt @@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { fun mapToUpdateContentValues(history: History) = ContentValues(1).apply { put(HistoryTable.COL_LAST_READ, history.last_read) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/LibraryMangaGetResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/LibraryMangaGetResolver.kt index 77369827a8..aac8ead3e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/LibraryMangaGetResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/LibraryMangaGetResolver.kt @@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver(), BaseMangaGet return manga } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterGetResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterGetResolver.kt index 4f9ce536fe..edd6a8983d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterGetResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterGetResolver.kt @@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver() { return MangaChapter(manga, chapter) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt index a87afe78c5..91aedffbbe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt @@ -5,7 +5,11 @@ import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver +import eu.kanade.tachiyomi.data.database.models.ChapterImpl +import eu.kanade.tachiyomi.data.database.models.HistoryImpl import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.data.database.tables.ChapterTable +import eu.kanade.tachiyomi.data.database.tables.HistoryTable class MangaChapterHistoryGetResolver : DefaultGetResolver() { companion object { @@ -35,15 +39,24 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver() val manga = mangaGetResolver.mapFromCursor(cursor) // Get chapter object - val chapter = chapterResolver.mapFromCursor(cursor) + val chapter = + if (!cursor.isNull(cursor.getColumnIndex(ChapterTable.COL_MANGA_ID))) chapterResolver + .mapFromCursor( + cursor + ) else ChapterImpl() // Get history object - val history = historyGetResolver.mapFromCursor(cursor) + val history = + if (!cursor.isNull(cursor.getColumnIndex(HistoryTable.COL_ID))) historyGetResolver.mapFromCursor( + cursor + ) else HistoryImpl() // Make certain column conflicts are dealt with - manga.id = chapter.manga_id - manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl")) - chapter.id = history.chapter_id + if (chapter.id != null) { + manga.id = chapter.manga_id + manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl")) + } + if (history.id != null) chapter.id = history.chapter_id // Return result return MangaChapterHistory(manga, chapter, history) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaHideTitlePutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaDateAddedPutResolver.kt similarity index 86% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaHideTitlePutResolver.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaDateAddedPutResolver.kt index 48ead1a5e6..27a51b3046 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaHideTitlePutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaDateAddedPutResolver.kt @@ -6,11 +6,10 @@ 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.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.tables.MangaTable -class MangaHideTitlePutResolver : PutResolver() { +class MangaDateAddedPutResolver : PutResolver() { override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { val updateQuery = mapToUpdateQuery(manga) @@ -27,7 +26,6 @@ class MangaHideTitlePutResolver : PutResolver() { .build() fun mapToContentValues(manga: Manga) = ContentValues(1).apply { - put(MangaTable.COL_HIDE_TITLE, manga.hide_title) + put(MangaTable.COL_DATE_ADDED, manga.date_added) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt index c0057d2135..a2d23f5051 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt @@ -28,6 +28,4 @@ class MangaFavoritePutResolver : PutResolver() { fun mapToContentValues(manga: Manga) = ContentValues(1).apply { put(MangaTable.COL_FAVORITE, manga.favorite) } - } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFlagsPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFlagsPutResolver.kt index 0c9b28c52d..4ed0a07288 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFlagsPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFlagsPutResolver.kt @@ -28,6 +28,4 @@ class MangaFlagsPutResolver : PutResolver() { fun mapToContentValues(manga: Manga) = ContentValues(1).apply { put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) } - } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt index 84afbd8139..eb50bf2f94 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt @@ -9,7 +9,7 @@ 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() { +class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver() { override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { val updateQuery = mapToUpdateQuery(manga) @@ -34,12 +34,11 @@ class MangaInfoPutResolver(val reset:Boolean = false): PutResolver() { } fun resetToContentValues(manga: Manga) = ContentValues(1).apply { - put(MangaTable.COL_TITLE, manga.originalTitle()) - put(MangaTable.COL_GENRE, manga.originalGenres()) - put(MangaTable.COL_AUTHOR, manga.originalAuthor()) - put(MangaTable.COL_ARTIST, manga.originalArtist()) - put(MangaTable.COL_DESCRIPTION, manga.originalDesc()) + 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()) } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt index 8b2672ea98..b1e5e78165 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt @@ -28,6 +28,4 @@ class MangaLastUpdatedPutResolver : PutResolver() { fun mapToContentValues(manga: Manga) = ContentValues(1).apply { put(MangaTable.COL_LAST_UPDATE, manga.last_update) } - } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaTitlePutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaTitlePutResolver.kt index 702173afbe..0ffb18ffd1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaTitlePutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaTitlePutResolver.kt @@ -28,5 +28,4 @@ class MangaTitlePutResolver : PutResolver() { fun mapToContentValues(manga: Manga) = ContentValues(1).apply { put(MangaTable.COL_TITLE, manga.title) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaViewerPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaViewerPutResolver.kt index e40f397a8d..a6b7c9b195 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaViewerPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaViewerPutResolver.kt @@ -28,5 +28,4 @@ class MangaViewerPutResolver : PutResolver() { fun mapToContentValues(manga: Manga) = ContentValues(1).apply { put(MangaTable.COL_VIEWER, manga.viewer) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt index 675103c974..28a565d219 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt @@ -23,7 +23,6 @@ object CategoryTable { $COL_MANGA_ORDER TEXT NOT NULL )""" - val addMangaOrder: String get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt index a2350caa0b..803714a845 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt @@ -24,6 +24,8 @@ object ChapterTable { const val COL_LAST_PAGE_READ = "last_page_read" + const val COL_PAGES_LEFT = "pages_left" + const val COL_CHAPTER_NUMBER = "chapter_number" const val COL_SOURCE_ORDER = "source_order" @@ -38,6 +40,7 @@ object ChapterTable { $COL_READ BOOLEAN NOT NULL, $COL_BOOKMARK BOOLEAN NOT NULL, $COL_LAST_PAGE_READ INT NOT NULL, + $COL_PAGES_LEFT INT NOT NULL, $COL_CHAPTER_NUMBER FLOAT NOT NULL, $COL_SOURCE_ORDER INTEGER NOT NULL, $COL_DATE_FETCH LONG NOT NULL, @@ -62,4 +65,6 @@ object ChapterTable { val addScanlator: String get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL" + val pagesLeftQuery: String + get() = "ALTER TABLE $TABLE ADD COLUMN $COL_PAGES_LEFT INTEGER DEFAULT 0" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt index a42e91e925..d0d34bbb78 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt @@ -20,5 +20,4 @@ object MangaCategoryTable { FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) ON DELETE CASCADE )""" - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt index c642536b01..2ea04b2379 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt @@ -40,6 +40,8 @@ object MangaTable { const val COL_HIDE_TITLE = "hideTitle" + const val COL_DATE_ADDED = "date_added" + val createTableQuery: String get() = """CREATE TABLE $TABLE( $COL_ID INTEGER NOT NULL PRIMARY KEY, @@ -57,7 +59,9 @@ object MangaTable { $COL_INITIALIZED BOOLEAN NOT NULL, $COL_VIEWER INTEGER NOT NULL, $COL_HIDE_TITLE INTEGER NOT NULL, - $COL_CHAPTER_FLAGS INTEGER NOT NULL + $COL_CHAPTER_FLAGS INTEGER NOT NULL, + $COL_DATE_ADDED LONG + )""" val createUrlIndexQuery: String @@ -69,4 +73,7 @@ object MangaTable { val addHideTitle: String get() = "ALTER TABLE $TABLE ADD COLUMN $COL_HIDE_TITLE INTEGER DEFAULT 0" + + val addDateAddedCol: String + get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG DEFAULT 0" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/SearchMetadataTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/SearchMetadataTable.kt index 9bcd2895d0..b7ac1744aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/SearchMetadataTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/SearchMetadataTable.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.database.tables - object SearchMetadataTable { const val TABLE = "search_metadata" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index a28e6b581a..d5c3c9fd08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -27,10 +27,10 @@ import java.util.concurrent.TimeUnit * @param preferences the preferences of the app. */ class DownloadCache( - private val context: Context, - private val provider: DownloadProvider, - private val sourceManager: SourceManager, - private val preferences: PreferencesHelper = Injekt.get() + private val context: Context, + private val provider: DownloadProvider, + private val sourceManager: SourceManager, + private val preferences: PreferencesHelper = Injekt.get() ) { /** @@ -78,7 +78,9 @@ class DownloadCache( checkRenew() val files = mangaFiles[manga.id] ?: return false - return files.any { it in provider.getValidChapterDirNames(chapter) } + return files.any { file -> provider.getValidChapterDirNames(chapter).any { + it.toLowerCase() == file.toLowerCase() + } } } /** @@ -117,7 +119,7 @@ class DownloadCache( onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id } - val db:DatabaseHelper by injectLazy() + val db: DatabaseHelper by injectLazy() val mangas = db.getMangas().executeAsBlocking().groupBy { it.source } sourceDirs.forEach { sourceValue -> @@ -140,10 +142,10 @@ class DownloadCache( } val trueMangaDirs = mangaDirs.mapNotNull { mangaDir -> val manga = sourceMangas.firstOrNull()?.find { DiskUtil.buildValidFilename( - it.originalTitle()).toLowerCase() == mangaDir.key - .toLowerCase() && it.source == sourceValue.key } ?: - sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename( - it.originalTitle()).toLowerCase() == mangaDir.key + it.title).toLowerCase() == mangaDir.key + .toLowerCase() && it.source == sourceValue.key } + ?: sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename( + it.title).toLowerCase() == mangaDir.key .toLowerCase() && it.source == sourceValue.key } val id = manga?.id ?: return@mapNotNull null id to mangaDir.value.files @@ -167,8 +169,7 @@ class DownloadCache( val files = mangaFiles[id] if (files == null) { mangaFiles[id] = mutableSetOf(chapterDirName) - } - else { + } else { mangaFiles[id]?.add(chapterDirName) } } @@ -214,7 +215,6 @@ class DownloadCache( sourceDir.files = list }*/ - /** * Removes a manga that has been deleted from this cache. * @@ -228,20 +228,26 @@ class DownloadCache( /** * Class to store the files under the root downloads directory. */ - private class RootDirectory(val dir: UniFile, - var files: Map = hashMapOf()) + private class RootDirectory( + val dir: UniFile, + var files: Map = hashMapOf() + ) /** * Class to store the files under a source directory. */ - private class SourceDirectory(val dir: UniFile, - var files: Map> = hashMapOf()) + private class SourceDirectory( + val dir: UniFile, + var files: Map> = hashMapOf() + ) /** * Class to store the files under a manga directory. */ - private class MangaDirectory(val dir: UniFile, - var files: MutableSet = hashSetOf()) + private class MangaDirectory( + val dir: UniFile, + var files: MutableSet = hashSetOf() + ) /** * Returns a new map containing only the key entries of [transform] that are not null. @@ -265,5 +271,4 @@ class DownloadCache( } return destination } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 5c89f19f5a..6d44a03455 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -96,6 +96,21 @@ class DownloadManager(val context: Context) { fun clearQueue(isNotification: Boolean = false) { deletePendingDownloads(*downloader.queue.toTypedArray()) downloader.clearQueue(isNotification) + DownloadService.callListeners(false) + } + + fun startDownloadNow(chapter: Chapter) { + val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return + val queue = downloader.queue.toMutableList() + queue.remove(download) + queue.add(0, download) + reorderQueue(queue) + if (isPaused()) { + if (DownloadService.isRunning(context)) + downloader.start() + else + DownloadService.start(context) + } } /** @@ -113,13 +128,14 @@ class DownloadManager(val context: Context) { downloader.pause() downloader.queue.clear() downloader.queue.addAll(downloads) - if(!wasPaused){ + if (!wasPaused) { downloader.start() } } fun isPaused() = downloader.isPaused() + fun hasQueue() = downloader.queue.isNotEmpty() /** * Tells the downloader to enqueue the given list of chapters. @@ -219,11 +235,13 @@ class DownloadManager(val context: Context) { } downloader.pause() downloader.queue.remove(chapters) - if(!wasPaused && downloader.queue.isNotEmpty()){ + if (!wasPaused && downloader.queue.isNotEmpty()) { downloader.start() - } - else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) { + } else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) { DownloadService.stop(context) + } else if (downloader.queue.isEmpty()) { + DownloadService.callListeners(false) + downloader.stop() } queue.remove(chapters) val chapterDirs = provider.findChapterDirs(chapters, manga, source) + provider.findTempChapterDirs(chapters, manga, source) @@ -253,7 +271,7 @@ class DownloadManager(val context: Context) { cleaned += readChapterDirs.size cache.removeChapters(readChapters, manga) if (cache.getDownloadCount(manga) == 0) { - provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete()// Delete manga directory if empty + provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty } return cleaned } @@ -292,4 +310,6 @@ class DownloadManager(val context: Context) { } } + fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener) + fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt index 8a2ea643cd..decfb110be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -6,7 +6,6 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications @@ -80,34 +79,32 @@ internal class DownloadNotifier(private val context: Context) { isDownloading = true // Pause action addAction(R.drawable.ic_av_pause_grey_24dp_img, - context.getString(R.string.action_pause), + context.getString(R.string.pause), NotificationReceiver.pauseDownloadsPendingBroadcast(context)) } if (download != null) { - val title = download.manga.currentTitle().chop(15) + val title = download.manga.title.chop(15) val quotedTitle = Pattern.quote(title) val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*" .toRegex(RegexOption.IGNORE_CASE), "") setContentTitle("$title - $chapter".chop(30)) setContentText( - context.getString(R.string.chapter_downloading) + context.getString(R.string.downloading) ) - } - else { + } else { setContentTitle( context.getString( - R.string.chapter_downloading + R.string.downloading ) ) setContentText(null) } - setProgress(0,0, true) + setProgress(0, 0, true) setStyle(null) } // Displays the progress bar on notification notification.show() - } /** @@ -128,15 +125,15 @@ internal class DownloadNotifier(private val context: Context) { isDownloading = true // Pause action addAction(R.drawable.ic_av_pause_grey_24dp_img, - context.getString(R.string.action_pause), + context.getString(R.string.pause), NotificationReceiver.pauseDownloadsPendingBroadcast(context)) } - val title = download.manga.currentTitle().chop(15) + val title = download.manga.title.chop(15) val quotedTitle = Pattern.quote(title) val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") setContentTitle("$title - $chapter".chop(30)) - setContentText(context.getString(R.string.chapter_downloading_progress) + setContentText(context.getString(R.string.downloading_progress) .format(download.downloadedImages, download.pages!!.size)) setStyle(null) setProgress(download.pages!!.size, download.downloadedImages, false) @@ -150,8 +147,8 @@ internal class DownloadNotifier(private val context: Context) { */ fun onDownloadPaused() { with(notification) { - setContentTitle(context.getString(R.string.chapter_paused)) - setContentText(context.getString(R.string.download_notifier_download_paused)) + setContentTitle(context.getString(R.string.paused)) + setContentText(context.getString(R.string.download_paused)) setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img) setAutoCancel(false) setProgress(0, 0, false) @@ -161,13 +158,13 @@ internal class DownloadNotifier(private val context: Context) { // Resume action addAction( R.drawable.ic_av_play_arrow_grey_img, - context.getString(R.string.action_resume), + context.getString(R.string.resume), NotificationReceiver.resumeDownloadsPendingBroadcast(context) ) - //Clear action + // Clear action addAction( R.drawable.ic_clear_grey_24dp_img, - context.getString(R.string.action_cancel_all), + context.getString(R.string.cancel_all), NotificationReceiver.clearDownloadsPendingBroadcast(context) ) } @@ -186,7 +183,7 @@ internal class DownloadNotifier(private val context: Context) { */ fun onWarning(reason: String) { with(notification) { - setContentTitle(context.getString(R.string.download_notifier_downloader_title)) + setContentTitle(context.getString(R.string.downloads)) setContentText(reason) setSmallIcon(android.R.drawable.stat_sys_warning) setAutoCancel(true) @@ -210,9 +207,9 @@ internal class DownloadNotifier(private val context: Context) { fun onError(error: String? = null, chapter: String? = null) { // Create notification with(notification) { - setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title)) - setContentText(error ?: context.getString(R.string.download_notifier_unkown_error)) - setStyle(NotificationCompat.BigTextStyle().bigText(error ?: context.getString(R.string.download_notifier_unkown_error))) + setContentTitle(chapter ?: context.getString(R.string.download_error)) + setContentText(error ?: context.getString(R.string.could_not_download_unexpected_error)) + setStyle(NotificationCompat.BigTextStyle().bigText(error ?: context.getString(R.string.could_not_download_unexpected_error))) setSmallIcon(android.R.drawable.stat_sys_warning) setCategory(NotificationCompat.CATEGORY_ERROR) clearActions() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt index 1a0b29311a..d81ae629a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadPendingDeleter.kt @@ -136,28 +136,28 @@ class DownloadPendingDeleter(context: Context) { * Class used to save an entry of chapters with their manga into preferences. */ private data class Entry( - val chapters: List, - val manga: MangaEntry + val chapters: List, + val manga: MangaEntry ) /** * Class used to save an entry for a chapter into preferences. */ private data class ChapterEntry( - val id: Long, - val url: String, - val name: String, - val scanlator: String? + val id: Long, + val url: String, + val name: String, + val scanlator: String? ) /** * Class used to save an entry for a manga into preferences. */ private data class MangaEntry( - val id: Long, - val url: String, - val title: String, - val source: Long + val id: Long, + val url: String, + val title: String, + val source: Long ) /** @@ -194,5 +194,4 @@ class DownloadPendingDeleter(context: Context) { it.name = name } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 03990d25ba..8702bbf3e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -37,9 +37,8 @@ class DownloadProvider(private val context: Context) { } init { - preferences.downloadsDirectory().asObservable() - .skip(1) - .subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) } + preferences.downloadsDirectory().asObservable().skip(1) + .subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) } } /** @@ -50,11 +49,10 @@ class DownloadProvider(private val context: Context) { */ internal fun getMangaDir(manga: Manga, source: Source): UniFile { try { - return downloadsDir - .createDirectory(getSourceDirName(source)) - .createDirectory(getMangaDirName(manga)) + return downloadsDir.createDirectory(getSourceDirName(source)) + .createDirectory(getMangaDirName(manga)) } catch (e: NullPointerException) { - throw Exception(context.getString(R.string.invalid_download_dir)) + throw Exception(context.getString(R.string.invalid_download_location)) } } @@ -136,8 +134,6 @@ class DownloadProvider(private val context: Context) { val sourceDir = findSourceDir(source) val mangaDir = sourceDir?.findFile(DiskUtil.buildValidFilename(from)) mangaDir?.renameTo(to) - // val downloadManager:DownloadManager by injectLazy() - // downloadManager.renameCache(from, to, sourceId) } /** @@ -147,7 +143,11 @@ class DownloadProvider(private val context: Context) { * @param manga the manga of the chapter. * @param source the source of the chapter. */ - fun findUnmatchedChapterDirs(chapters: List, manga: Manga, source: Source): List { + fun findUnmatchedChapterDirs( + chapters: List, + manga: Manga, + source: Source + ): List { val mangaDir = findMangaDir(manga, source) ?: return emptyList() return mangaDir.listFiles()!!.asList().filter { (chapters.find { chp -> @@ -170,7 +170,6 @@ class DownloadProvider(private val context: Context) { return chapters.mapNotNull { mangaDir.findFile("${getChapterDirName(it)}_tmp") } } - /** * Returns the download directory name for a source. * @@ -186,7 +185,7 @@ class DownloadProvider(private val context: Context) { * @param manga the manga to query. */ fun getMangaDirName(manga: Manga): String { - return DiskUtil.buildValidFilename(manga.originalTitle()) + return DiskUtil.buildValidFilename(manga.title) } /** @@ -213,5 +212,4 @@ class DownloadProvider(private val context: Context) { DiskUtil.buildValidFilename(chapter.name) ) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt index 0e287e0b5c..6697f4eafe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -40,12 +40,29 @@ class DownloadService : Service() { */ val runningRelay: BehaviorRelay = BehaviorRelay.create(false) + private val listeners = mutableSetOf() + + fun addListener(listener: DownloadServiceListener) { + listeners.add(listener) + } + + fun removeListener(listener: DownloadServiceListener) { + listeners.remove(listener) + } + + fun callListeners(downloading: Boolean? = null) { + val downloadManager: DownloadManager by injectLazy() + listeners.forEach { + it.downloadStatusChanged(downloading ?: downloadManager.hasQueue()) + } + } /** * Starts this service. * * @param context the application context. */ fun start(context: Context) { + callListeners() val intent = Intent(context, DownloadService::class.java) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { context.startService(intent) @@ -116,6 +133,7 @@ class DownloadService : Service() { runningRelay.call(false) subscriptions.unsubscribe() downloadManager.stopDownloads() + callListeners(downloadManager.hasQueue()) wakeLock.releaseIfNeeded() super.onDestroy() } @@ -124,7 +142,7 @@ class DownloadService : Service() { * Not used. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - return Service.START_NOT_STICKY + return START_NOT_STICKY } /** @@ -145,7 +163,7 @@ class DownloadService : Service() { .observeOn(AndroidSchedulers.mainThread()) .subscribe({ state -> onNetworkStateChanged(state) }, { - toast(R.string.download_queue_error) + toast(R.string.could_not_download_chapter_can_try_again) stopSelf() }) } @@ -159,14 +177,14 @@ class DownloadService : Service() { when (connectivity.state) { CONNECTED -> { if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) { - downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi)) + downloadManager.stopDownloads(getString(R.string.no_wifi_connection)) } else { val started = downloadManager.startDownloads() if (!started) stopSelf() } } DISCONNECTED -> { - downloadManager.stopDownloads(getString(R.string.download_notifier_no_network)) + downloadManager.stopDownloads(getString(R.string.no_network_connection)) } else -> { /* Do nothing */ } } @@ -200,8 +218,11 @@ class DownloadService : Service() { private fun getPlaceholderNotification(): Notification { return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER) - .setContentTitle(getString(R.string.download_notifier_downloader_title)) + .setContentTitle(getString(R.string.downloading)) .build() } +} +interface DownloadServiceListener { + fun downloadStatusChanged(downloading: Boolean) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt index e9754053c0..cfa82a2753 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt @@ -15,8 +15,8 @@ import uy.kohesive.injekt.injectLazy * @param context the application context. */ class DownloadStore( - context: Context, - private val sourceManager: SourceManager + context: Context, + private val sourceManager: SourceManager ) { /** @@ -133,5 +133,4 @@ class DownloadStore( * @param order the order of the download in the queue. */ data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int) - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 058ac5acb4..667320d01d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue +import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource @@ -45,10 +46,10 @@ import java.io.File * @param sourceManager the source manager. */ class Downloader( - private val context: Context, - private val provider: DownloadProvider, - private val cache: DownloadCache, - private val sourceManager: SourceManager + private val context: Context, + private val provider: DownloadProvider, + private val cache: DownloadCache, + private val sourceManager: SourceManager ) { /** @@ -90,6 +91,7 @@ class Downloader( launchNow { val chapters = async { store.restore() } queue.addAll(chapters.await()) + DownloadService.callListeners() } } @@ -128,12 +130,11 @@ class Downloader( if (notifier.paused) { if (queue.isEmpty()) { notifier.dismiss() - } - else { + } else { notifier.paused = false notifier.onDownloadPaused() } - }else { + } else { notifier.dismiss() } } @@ -163,7 +164,7 @@ class Downloader( fun clearQueue(isNotification: Boolean = false) { destroySubscriptions() - //Needed to update the chapter view + // Needed to update the chapter view if (isNotification) { queue .filter { it.status == Download.QUEUE } @@ -179,7 +180,7 @@ class Downloader( * @param isNotification value that determines if status is set (needed for view updates) */ fun clearQueue(manga: Manga, isNotification: Boolean = false) { - //Needed to update the chapter view + // Needed to update the chapter view if (isNotification) { queue .filter { it.status == Download.QUEUE && it.manga.id == manga.id } @@ -263,6 +264,8 @@ class Downloader( // Start downloader if needed if (autoStart && wasEmpty) { DownloadService.start(this@Downloader.context) + } else if (!isRunning && !LibraryUpdateService.isRunning()) { + notifier.onDownloadPaused() } } } @@ -317,7 +320,6 @@ class Downloader( notifier.onError(error.message, download.chapter.name) download } - } /** @@ -447,8 +449,12 @@ class Downloader( * @param tmpDir the directory where the download is currently stored. * @param dirname the real (non temporary) directory name of the download. */ - private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile, - tmpDir: UniFile, dirname: String) { + private fun ensureSuccessfulDownload( + download: Download, + mangaDir: UniFile, + tmpDir: UniFile, + dirname: String + ) { // Ensure that the chapter folder has all the images. val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } @@ -496,5 +502,4 @@ class Downloader( companion object { const val TMP_DIR_SUFFIX = "_tmp" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index 9c2b503e93..6701d43a09 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import rx.subjects.PublishSubject +import kotlin.math.roundToInt class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { @@ -18,20 +19,39 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { set(status) { field = status statusSubject?.onNext(this) + statusCallback?.invoke(this) } @Transient private var statusSubject: PublishSubject? = null + @Transient private var statusCallback: ((Download) -> Unit)? = null + + val pageProgress: Int + get() { + val pages = pages ?: return 0 + return pages.map(Page::progress).sum() + } + + val progress: Int + get() { + val pages = pages ?: return 0 + return pages.map(Page::progress).average().roundToInt() + } + fun setStatusSubject(subject: PublishSubject?) { statusSubject = subject } - companion object { + fun setStatusCallback(f: ((Download) -> Unit)?) { + statusCallback = f + } + companion object { + const val CHECKED = -1 const val NOT_DOWNLOADED = 0 const val QUEUE = 1 const val DOWNLOADING = 2 const val DOWNLOADED = 3 const val ERROR = 4 } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index 197140d0f8..c3e2c72bb7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -10,17 +10,21 @@ import rx.subjects.PublishSubject import java.util.concurrent.CopyOnWriteArrayList class DownloadQueue( - private val store: DownloadStore, - private val queue: MutableList = CopyOnWriteArrayList()) -: List by queue { + private val store: DownloadStore, + private val queue: MutableList = CopyOnWriteArrayList() +) : +List by queue { private val statusSubject = PublishSubject.create() private val updatedRelay = PublishRelay.create() + private val downloadListeners = mutableListOf() + fun addAll(downloads: List) { downloads.forEach { download -> download.setStatusSubject(statusSubject) + download.setStatusCallback(::setPagesFor) download.status = Download.QUEUE } queue.addAll(downloads) @@ -32,6 +36,10 @@ class DownloadQueue( val removed = queue.remove(download) store.remove(download) download.setStatusSubject(null) + download.setStatusCallback(null) + if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) + download.status = Download.NOT_DOWNLOADED + downloadListeners.forEach { it.updateDownload(download) } if (removed) { updatedRelay.call(Unit) } @@ -52,6 +60,10 @@ class DownloadQueue( fun clear() { queue.forEach { download -> download.setStatusSubject(null) + download.setStatusCallback(null) + if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) + download.status = Download.NOT_DOWNLOADED + downloadListeners.forEach { it.updateDownload(download) } } queue.clear() store.clear() @@ -67,6 +79,26 @@ class DownloadQueue( .startWith(Unit) .map { this } + private fun setPagesFor(download: Download) { + if (download.status == Download.DOWNLOADING) { + if (download.pages != null) + for (page in download.pages!!) + page.setStatusCallback { + callListeners(download) + } + downloadListeners.forEach { it.updateDownload(download) } + } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { + setPagesSubject(download.pages, null) + downloadListeners.forEach { it.updateDownload(download) } + } else { + downloadListeners.forEach { it.updateDownload(download) } + } + } + + private fun callListeners(download: Download) { + downloadListeners.forEach { it.updateDownload(download) } + } + fun getProgressObservable(): Observable { return statusSubject.onBackpressureBuffer() .startWith(getActiveDownloads()) @@ -74,13 +106,14 @@ class DownloadQueue( if (download.status == Download.DOWNLOADING) { val pageStatusSubject = PublishSubject.create() setPagesSubject(download.pages, pageStatusSubject) + downloadListeners.forEach { it.updateDownload(download) } return@flatMap pageStatusSubject .onBackpressureBuffer() .filter { it == Page.READY } .map { download } - } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { setPagesSubject(download.pages, null) + downloadListeners.forEach { it.updateDownload(download) } } Observable.just(download) } @@ -95,4 +128,15 @@ class DownloadQueue( } } + fun addListener(listener: DownloadListener) { + downloadListeners.add(listener) + } + + fun removeListener(listener: DownloadListener) { + downloadListeners.remove(listener) + } + + interface DownloadListener { + fun updateDownload(download: Download) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt index a795e2e059..6f88ec722d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt @@ -5,7 +5,11 @@ import android.util.Log import com.bumptech.glide.Priority import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.data.DataFetcher -import java.io.* +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream open class FileFetcher(private val file: File) : DataFetcher { @@ -48,4 +52,4 @@ open class FileFetcher(private val file: File) : DataFetcher { override fun getDataSource(): DataSource { return DataSource.LOCAL } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt index 5fec42af7a..f27f6e5363 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt @@ -16,10 +16,12 @@ import java.io.InputStream * @param manga the manga of the cover to load. * @param file the file where this cover should be. It may exists or not. */ -class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher, - private val manga: Manga, - private val file: File) -: FileFetcher(file) { +class LibraryMangaUrlFetcher( + private val networkFetcher: DataFetcher, + private val manga: Manga, + private val file: File +) : +FileFetcher(file) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { if (!file.exists()) { @@ -52,7 +54,6 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher { * @param width the width of the view where the resource will be loaded. * @param height the height of the view where the resource will be loaded. */ - override fun buildLoadData(manga: Manga, width: Int, height: Int, - options: Options): ModelLoader.LoadData? { + override fun buildLoadData( + manga: Manga, + width: Int, + height: Int, + options: Options + ): ModelLoader.LoadData? { // Check thumbnail is not null or empty val url = manga.thumbnail_url - if (url == null || url.isEmpty()) { - return null - } - if (url.startsWith("http")) { + if (url?.startsWith("http") == true) { val source = sourceManager.get(manga.source) as? HttpSource val glideUrl = GlideUrl(url, getHeaders(manga, source)) @@ -105,8 +110,14 @@ class MangaModelLoader : ModelLoader { // Return an instance of the fetcher providing the needed elements. return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher) } else { - // Get the file from the url, removing the scheme if present. - val file = File(url.substringAfter("file://")) + // Get the file from the url, removing the scheme if present, or from the cache if no url. + val file = when { + manga.hasCustomCover() -> coverCache.getCoverFile(manga.thumbnail_url!!) + url != null -> File(url.substringAfter("file://")) + else -> null + } + + if (file?.exists() != true) return null // Return an instance of the fetcher providing the needed elements. return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file)) @@ -142,5 +153,4 @@ class MangaModelLoader : ModelLoader { value } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt index aa3ebf6f95..cdf880e426 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt @@ -24,4 +24,4 @@ class MangaSignature(manga: Manga, file: File) : Key { override fun updateDiskCacheKey(md: MessageDigest) { md.update(key.toByteArray(Key.CHARSET)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt index bb117086e3..dd6d546f8c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt @@ -14,10 +14,10 @@ import java.io.InputStream class PassthroughModelLoader : ModelLoader { override fun buildLoadData( - model: InputStream, - width: Int, - height: Int, - options: Options + model: InputStream, + width: Int, + height: Int, + options: Options ): ModelLoader.LoadData? { return ModelLoader.LoadData(ObjectKey(model), Fetcher(model)) } @@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader { } override fun loadData( - priority: Priority, - callback: DataFetcher.DataCallback + priority: Priority, + callback: DataFetcher.DataCallback ) { callback.onDataReady(stream) } - } /** @@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader { class Factory : ModelLoaderFactory { override fun build( - multiFactory: MultiModelLoaderFactory + multiFactory: MultiModelLoaderFactory ): ModelLoader { return PassthroughModelLoader() } override fun teardown() {} } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt index 1eecf3eedb..fffdbc433e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.glide import android.content.Context -import android.graphics.drawable.Drawable import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import com.bumptech.glide.Registry @@ -10,7 +9,6 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.request.RequestOptions import eu.kanade.tachiyomi.data.database.models.Manga @@ -28,8 +26,6 @@ class TachiGlideModule : AppGlideModule() { override fun applyOptions(context: Context, builder: GlideBuilder) { builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) - builder.setDefaultTransitionOptions(Drawable::class.java, - DrawableTransitionOptions.withCrossFade()) } override fun registerComponents(context: Context, glide: Glide, registry: Registry) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateRanker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateRanker.kt index d53cb5c90a..c7f9699d9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateRanker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateRanker.kt @@ -36,8 +36,7 @@ object LibraryUpdateRanker { fun lexicographicRanking(): Comparator { return Comparator { mangaFirst: Manga, mangaSecond: Manga -> - compareValues(mangaFirst.currentTitle(), mangaSecond.currentTitle()) + compareValues(mangaFirst.title, mangaSecond.title) } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 37adc99fa4..60190eb126 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -31,21 +31,20 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager -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.ui.main.MainActivity +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.lang.chop +import eu.kanade.tachiyomi.util.system.executeOnIO +import eu.kanade.tachiyomi.util.system.notification +import eu.kanade.tachiyomi.util.system.notificationManager import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource -import eu.kanade.tachiyomi.util.lang.chop -import eu.kanade.tachiyomi.util.system.isServiceRunning -import eu.kanade.tachiyomi.util.system.notification -import eu.kanade.tachiyomi.util.system.notificationManager +import kotlinx.coroutines.withContext import rx.Observable import rx.Subscription import rx.schedulers.Schedulers @@ -66,11 +65,11 @@ import java.util.concurrent.atomic.AtomicInteger * destroyed. */ class LibraryUpdateService( - val db: DatabaseHelper = Injekt.get(), - val sourceManager: SourceManager = Injekt.get(), - val preferences: PreferencesHelper = Injekt.get(), - val downloadManager: DownloadManager = Injekt.get(), - val trackManager: TrackManager = Injekt.get() + val db: DatabaseHelper = Injekt.get(), + val sourceManager: SourceManager = Injekt.get(), + val preferences: PreferencesHelper = Injekt.get(), + val downloadManager: DownloadManager = Injekt.get(), + val trackManager: TrackManager = Injekt.get() ) : Service() { /** @@ -83,7 +82,6 @@ class LibraryUpdateService( */ private var subscription: Subscription? = null - /** * Pending intent of action that cancels the library update */ @@ -98,19 +96,31 @@ class LibraryUpdateService( BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) } - private var job:Job? = null + private var job: Job? = null + + private val mangaToUpdate = mutableListOf() + + private val categoryIds = mutableSetOf() + + // List containing new updates + private val newUpdates = mutableMapOf>() /** * Cached progress notification to avoid creating a lot. */ - private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY) + private val progressNotification by lazy { + NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY) .setContentTitle(getString(R.string.app_name)) .setSmallIcon(R.drawable.ic_refresh_white_24dp_img) .setLargeIcon(notificationBitmap) .setOngoing(true) .setOnlyAlertOnce(true) .setColor(ContextCompat.getColor(this, R.color.colorAccent)) - .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) + .addAction( + R.drawable.ic_clear_grey_24dp_img, + getString(android.R.string.cancel), + cancelIntent + ) } /** @@ -118,8 +128,8 @@ class LibraryUpdateService( */ enum class Target { CHAPTERS, // Manga chapters - DETAILS, // Manga metadata - TRACKING // Tracking metadata + DETAILS, // Manga metadata + TRACKING // Tracking metadata } companion object { @@ -129,11 +139,8 @@ class LibraryUpdateService( */ const val KEY_CATEGORY = "category" - private val mangaToUpdate = mutableListOf() - - private val categoryIds = mutableSetOf() - - fun categoryInQueue(id: Int?) = categoryIds.contains(id) + fun categoryInQueue(id: Int?) = instance?.categoryIds?.contains(id) ?: false + private var instance: LibraryUpdateService? = null /** * Key that defines what should be updated. @@ -143,11 +150,10 @@ class LibraryUpdateService( /** * Returns the status of the service. * - * @param context the application context. * @return true if the service is running, false otherwise. */ - fun isRunning(context: Context): Boolean { - return context.isServiceRunning(LibraryUpdateService::class.java) + fun isRunning(): Boolean { + return instance != null } /** @@ -159,12 +165,11 @@ class LibraryUpdateService( * @param target defines what should be updated. */ fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) { - if (!isRunning(context)) { + if (!isRunning()) { val intent = Intent(context, LibraryUpdateService::class.java).apply { putExtra(KEY_TARGET, target) category?.id?.let { id -> putExtra(KEY_CATEGORY, id) - categoryIds.add(id) } } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { @@ -172,67 +177,83 @@ class LibraryUpdateService( } else { context.startForegroundService(intent) } - } - else { + } else { if (target == Target.CHAPTERS) category?.id?.let { - categoryIds.add(it) - val preferences: PreferencesHelper = Injekt.get() - val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() - addManga(getMangaToUpdate(it, target).sortedWith( - rankingScheme[selectedScheme] - )) + instance?.addCategory(it) } } } - private fun addManga(mangaToAdd: List) { - for (manga in mangaToAdd) { - if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga) - } - } - /** * Stops the service. * * @param context the application context. */ fun stop(context: Context) { + instance?.job?.cancel() context.stopService(Intent(context, LibraryUpdateService::class.java)) } - /** - * Returns the list of manga to be updated. - * - * @param intent the update intent. - * @param target the target to update. - * @return a list of manga to update - */ - private fun getMangaToUpdate(categoryId: Int, target: Target): List { - val preferences: PreferencesHelper = Injekt.get() - val db: DatabaseHelper = Injekt.get() - var listToUpdate = if (categoryId != -1) - db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } - else { - val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt) - categoryIds.addAll(categoriesToUpdate) - if (categoriesToUpdate.isNotEmpty()) - db.getLibraryMangas().executeAsBlocking() - .filter { it.category in categoriesToUpdate } - .distinctBy { it.id } - else - db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } - } - if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { - listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } - } + private var listener: LibraryServiceListener? = null + + fun setListener(listener: LibraryServiceListener) { + this.listener = listener + } + + fun removeListener(listener: LibraryServiceListener) { + if (this.listener == listener) + this.listener = null + } + } - return listToUpdate + private fun addManga(mangaToAdd: List) { + for (manga in mangaToAdd) { + if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga) } + } + + private fun addCategory(categoryId: Int) { + val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() + val mangas = + getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith( + rankingScheme[selectedScheme] + ) + categoryIds.add(categoryId) + addManga(mangas) + } - private fun getMangaToUpdate(intent: Intent, target: Target): List { - val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) - return getMangaToUpdate(categoryId, target) + /** + * Returns the list of manga to be updated. + * + * @param intent the update intent. + * @param target the target to update. + * @return a list of manga to update + */ + private fun getMangaToUpdate(categoryId: Int, target: Target): List { + var listToUpdate = if (categoryId != -1) { + categoryIds.add(categoryId) + db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } + } else { + val categoriesToUpdate = + preferences.libraryUpdateCategories().getOrDefault().map(String::toInt) + categoryIds.addAll(categoriesToUpdate) + if (categoriesToUpdate.isNotEmpty()) + db.getLibraryMangas().executeAsBlocking() + .filter { it.category in categoriesToUpdate } + .distinctBy { it.id } + else + db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } + } + if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { + listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } } + + return listToUpdate + } + + private fun getMangaToUpdate(intent: Intent, target: Target): List { + val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) + return getMangaToUpdate(categoryId, target) } /** @@ -243,26 +264,24 @@ class LibraryUpdateService( super.onCreate() startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build()) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") + PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock" + ) wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)) } - override fun stopService(name: Intent?): Boolean { - job?.cancel() - return super.stopService(name) - } - /** * Method called when the service is destroyed. It destroys subscriptions and releases the wake * lock. */ override fun onDestroy() { + job?.cancel() + if (instance == this) + instance = null subscription?.unsubscribe() - mangaToUpdate.clear() - categoryIds.clear() if (wakeLock.isHeld) { wakeLock.release() } + listener?.onUpdateManga(LibraryManga()) super.onDestroy() } @@ -283,93 +302,78 @@ class LibraryUpdateService( */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) return START_NOT_STICKY - val target = intent.getSerializableExtra(KEY_TARGET) as? Target ?: return START_NOT_STICKY + val target = intent.getSerializableExtra(KEY_TARGET) as? Target + ?: return START_NOT_STICKY // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() + instance = this - val handler = CoroutineExceptionHandler { _, exception -> - Timber.e(exception) - stopSelf(startId) - } val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() - if (target == Target.CHAPTERS) { - updateChapters( - getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]), startId - ) - } - else { + val mangaList = + getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]) + // Update favorite manga. Destroy service when completed or in case of an error. + if (target == Target.DETAILS) { // Update either chapter list or manga details. - // Update favorite manga. Destroy service when completed or in case of an error. - val mangaList = - getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]) subscription = Observable.defer { - when (target) { - Target.DETAILS -> updateDetails(mangaList) - else -> updateTrackings(mangaList) - } + updateDetails(mangaList) }.subscribeOn(Schedulers.io()).subscribe({}, { Timber.e(it) stopSelf(startId) }, { stopSelf(startId) }) + } else { + launchTarget(target, mangaList, startId) } return START_REDELIVER_INTENT } - private fun updateChapters(mangaToAdd: List, startId: Int) { - addManga(mangaToAdd) - - if (job == null) { - job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) { - updateChaptersJob() - mangaToUpdate.clear() - categoryIds.clear() - stopSelf(startId) + private fun launchTarget(target: Target, mangaToAdd: List, startId: Int) { + val handler = CoroutineExceptionHandler { _, exception -> + Timber.e(exception) + stopSelf(startId) + } + if (target == Target.CHAPTERS) { + job = GlobalScope.launch(handler) { + updateChaptersJob(mangaToAdd) + } + } else { + job = GlobalScope.launch(handler) { + updateTrackings(mangaToAdd) } } + + job?.invokeOnCompletion { stopSelf(startId) } } - private fun updateChaptersJob() { - // Initialize the variables holding the progress of the updates. - var count = 0 - // List containing new updates - val newUpdates = ArrayList>>() - // list containing failed updates - val failedUpdates = ArrayList() + private suspend fun updateChaptersJob(mangaToAdd: List) { // List containing categories that get included in downloads. - val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt) + val categoriesToDownload = + preferences.downloadNewCategories().getOrDefault().map(String::toInt) // Boolean to determine if user wants to automatically download new chapters. val downloadNew = preferences.downloadNew().getOrDefault() // Boolean to determine if DownloadManager has downloads var hasDownloads = false + // Initialize the variables holding the progress of the updates. + var count = 0 + mangaToUpdate.addAll(mangaToAdd) while (count < mangaToUpdate.size) { - if (job?.isCancelled == true || job == null) break - val manga = mangaToUpdate[count] - showProgressNotification(manga, count++, mangaToUpdate.size) - val source = sourceManager.get(manga.source) as? HttpSource ?: continue - val fetchedChapters = try { source.fetchChapterList(manga).toBlocking().single() } - catch(e: java.lang.Exception) { - failedUpdates.add(manga) - emptyList() } - if (fetchedChapters.isNotEmpty()) { - val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source).first - if (newChapters.isNotEmpty()) { - if (downloadNew && (categoriesToDownload.isEmpty() || manga.category in categoriesToDownload)) { - downloadChapters(manga, newChapters.sortedBy { it.chapter_number }) - hasDownloads = true - } - newUpdates.add(manga to newChapters.sortedBy { it.chapter_number }.toTypedArray()) - } + val shouldDownload = (downloadNew && (categoriesToDownload.isEmpty() || + mangaToUpdate[count].category in categoriesToDownload || + db.getCategoriesForManga(mangaToUpdate[count]).executeOnIO() + .any { (it.id ?: -1) in categoriesToDownload })) + if (updateMangaChapters(mangaToUpdate[count], count, shouldDownload)) { + hasDownloads = true } + count++ } if (newUpdates.isNotEmpty()) { showResultNotification(newUpdates) - if (preferences.refreshCoversToo().getOrDefault()) { - updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io()) + if (preferences.refreshCoversToo().getOrDefault() && job?.isCancelled == false) { + updateDetails(newUpdates.map { it.key }).observeOn(Schedulers.io()) .doOnCompleted { cancelProgressNotification() if (downloadNew && hasDownloads) { @@ -377,19 +381,51 @@ class LibraryUpdateService( } } .subscribeOn(Schedulers.io()).subscribe {} - } - else if (downloadNew && hasDownloads) { + } else if (downloadNew && hasDownloads) { DownloadService.start(this) } } - if (failedUpdates.isNotEmpty()) { - Timber.e("Failed updating: ${failedUpdates.map { it.title }}") - } - cancelProgressNotification() } + private suspend fun updateMangaChapters( + manga: LibraryManga, + progess: Int, + shouldDownload: Boolean + ): + Boolean { + try { + var hasDownloads = false + if (job?.isCancelled == true) { + throw java.lang.Exception("Job was cancelled") + } + showProgressNotification(manga, progess, mangaToUpdate.size) + val source = sourceManager.get(manga.source) as? HttpSource ?: return false + val fetchedChapters = withContext(Dispatchers.IO) { + source.fetchChapterList(manga).toBlocking().single() + } ?: emptyList() + if (fetchedChapters.isNotEmpty()) { + val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source) + if (newChapters.first.isNotEmpty()) { + if (shouldDownload) { + downloadChapters(manga, newChapters.first.sortedBy { it.chapter_number }) + hasDownloads = true + } + newUpdates[manga] = + newChapters.first.sortedBy { it.chapter_number }.toTypedArray() + } + if (newChapters.first.size + newChapters.second.size > 0) listener?.onUpdateManga( + manga + ) + } + return hasDownloads + } catch (e: Exception) { + Timber.e("Failed updating: ${manga.title}: $e") + return false + } + } + fun downloadChapters(manga: Manga, chapters: List) { // we need to get the chapters from the db so we have chapter ids val mangaChapters = db.getChapters(manga).executeAsBlocking() @@ -410,7 +446,7 @@ class LibraryUpdateService( fun updateManga(manga: Manga): Observable, List>> { val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() return source.fetchChapterList(manga) - .map { syncChaptersWithSource(db, it, manga, source) } + .map { syncChaptersWithSource(db, it, manga, source) } } /** @@ -426,61 +462,57 @@ class LibraryUpdateService( // Emit each manga and update it sequentially. return Observable.from(mangaToUpdate) - // Notify manga that will update. - .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } - // Update the details of the manga. - .concatMap { manga -> - val source = sourceManager.get(manga.source) as? HttpSource - ?: return@concatMap Observable.empty() - - source.fetchMangaDetails(manga) - .map { networkManga -> - manga.copyFrom(networkManga) - db.insertManga(manga).executeAsBlocking() - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - manga - } - .onErrorReturn { manga } - } - .doOnCompleted { - cancelProgressNotification() - } + // Notify manga that will update. + .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } + // Update the details of the manga. + .concatMap { manga -> + val source = sourceManager.get(manga.source) as? HttpSource + ?: return@concatMap Observable.empty() + source.fetchMangaDetails(manga) + .map { networkManga -> + val thumbnailUrl = manga.thumbnail_url + manga.copyFrom(networkManga) + db.insertManga(manga).executeAsBlocking() + if (thumbnailUrl != networkManga.thumbnail_url) + MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + manga + } + .onErrorReturn { manga } + } + .doOnCompleted { + cancelProgressNotification() + } } /** * Method that updates the metadata of the connected tracking services. It's called in a * background thread, so it's safe to do heavy operations or network calls here. */ - private fun updateTrackings(mangaToUpdate: List): Observable { + + private suspend fun updateTrackings(mangaToUpdate: List) { // Initialize the variables holding the progress of the updates. var count = 0 val loggedServices = trackManager.services.filter { it.isLogged } - // Emit each manga and update it sequentially. - return Observable.from(mangaToUpdate) - // Notify manga that will update. - .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } - // Update the tracking details. - .concatMap { manga -> - val tracks = db.getTracks(manga).executeAsBlocking() - - Observable.from(tracks) - .concatMap { track -> - val service = trackManager.getService(track.sync_id) - if (service != null && service in loggedServices) { - service.refresh(track) - .doOnNext { db.insertTrack(it).executeAsBlocking() } - .onErrorReturn { track } - } else { - Observable.empty() - } - } - .map { manga } - } - .doOnCompleted { - cancelProgressNotification() + mangaToUpdate.forEach { manga -> + showProgressNotification(manga, count++, mangaToUpdate.size) + + val tracks = db.getTracks(manga).executeAsBlocking() + + tracks.forEach { track -> + val service = trackManager.getService(track.sync_id) + if (service != null && service in loggedServices) { + try { + service.refresh(track) + db.insertTrack(track).executeAsBlocking() + } catch (e: Exception) { + Timber.e(e) + } } + } + } + cancelProgressNotification() } /** @@ -491,10 +523,12 @@ class LibraryUpdateService( * @param total the total progress. */ private fun showProgressNotification(manga: Manga, current: Int, total: Int) { - notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification - .setContentTitle(manga.currentTitle()) + notificationManager.notify( + Notifications.ID_LIBRARY_PROGRESS, progressNotification + .setContentTitle(manga.title) .setProgress(total, current, false) - .build()) + .build() + ) } /** @@ -502,11 +536,11 @@ class LibraryUpdateService( * * @param updates a list of manga with new updates. */ - private fun showResultNotification(updates: List>>) { + private fun showResultNotification(updates: Map>) { val notifications = ArrayList>() updates.forEach { - val manga = it.first - val chapters = it.second + val manga = it.key + val chapters = it.value val chapterNames = chapters.map { chapter -> chapter.name } notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) { setSmallIcon(R.drawable.ic_tachi) @@ -515,15 +549,17 @@ class LibraryUpdateService( .asBitmap().load(manga).dontTransform().centerCrop().circleCrop() .override(256, 256).submit().get() setLargeIcon(icon) + } catch (e: Exception) { } - catch (e: Exception) { } setGroupAlertBehavior(GROUP_ALERT_SUMMARY) - setContentTitle(manga.currentTitle()) + setContentTitle(manga.title) color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent) val chaptersNames = if (chapterNames.size > 5) { "${chapterNames.take(4).joinToString(", ")}, " + - resources.getQuantityString(R.plurals.notification_and_n_more, - (chapterNames.size - 4), (chapterNames.size - 4)) + resources.getQuantityString( + R.plurals.notification_and_n_more, + (chapterNames.size - 4), (chapterNames.size - 4) + ) } else chapterNames.joinToString(", ") setContentText(chaptersNames) setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames)) @@ -534,41 +570,57 @@ class LibraryUpdateService( this@LibraryUpdateService, manga, chapters.first() ) ) - addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), - NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService, - manga, chapters, Notifications.ID_NEW_CHAPTERS)) - addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters), - NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, - manga, Notifications.ID_NEW_CHAPTERS)) + addAction( + R.drawable.ic_glasses_black_24dp, getString(R.string.mark_as_read), + NotificationReceiver.markAsReadPendingBroadcast( + this@LibraryUpdateService, + manga, chapters, Notifications.ID_NEW_CHAPTERS + ) + ) + addAction( + R.drawable.ic_book_white_24dp, getString(R.string.view_chapters), + NotificationReceiver.openChapterPendingActivity( + this@LibraryUpdateService, + manga, Notifications.ID_NEW_CHAPTERS + ) + ) setAutoCancel(true) }, manga.id.hashCode())) } NotificationManagerCompat.from(this).apply { - notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { - setSmallIcon(R.drawable.ic_tachi) - setLargeIcon(notificationBitmap) - setContentTitle(getString(R.string.notification_new_chapters)) - color = ContextCompat.getColor(applicationContext, R.color.colorAccent) - if (updates.size > 1) { - setContentText(resources.getQuantityString(R.plurals - .notification_new_chapters_text, - updates.size, updates.size)) - setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") { - it.first.currentTitle().chop(45) - })) - } - else { - setContentText(updates.first().first.currentTitle().chop(45)) - } - priority = NotificationCompat.PRIORITY_HIGH - setGroup(Notifications.GROUP_NEW_CHAPTERS) - setGroupAlertBehavior(GROUP_ALERT_SUMMARY) - setGroupSummary(true) - setContentIntent(getNotificationIntent()) - setAutoCancel(true) - }) + notify( + Notifications.ID_NEW_CHAPTERS, + notification(Notifications.CHANNEL_NEW_CHAPTERS) { + setSmallIcon(R.drawable.ic_tachi) + setLargeIcon(notificationBitmap) + setContentTitle(getString(R.string.new_chapters_found)) + color = ContextCompat.getColor(applicationContext, R.color.colorAccent) + if (updates.size > 1) { + setContentText( + resources.getQuantityString( + R.plurals + .for_n_titles, + updates.size, updates.size + ) + ) + setStyle( + NotificationCompat.BigTextStyle() + .bigText(updates.keys.joinToString("\n") { + it.title.chop(45) + }) + ) + } else { + setContentText(updates.keys.first().title.chop(45)) + } + priority = NotificationCompat.PRIORITY_HIGH + setGroup(Notifications.GROUP_NEW_CHAPTERS) + setGroupAlertBehavior(GROUP_ALERT_SUMMARY) + setGroupSummary(true) + setContentIntent(getNotificationIntent()) + setAutoCancel(true) + }) notifications.forEach { notify(it.second, it.first) @@ -592,5 +644,8 @@ class LibraryUpdateService( intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } +} +interface LibraryServiceListener { + fun onUpdateManga(manga: LibraryManga) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt index 2f474f3f5a..e43f3b1e1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt @@ -18,11 +18,10 @@ object NotificationHandler { * @param context context of application */ internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent { - val intent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT - action = MainActivity.SHORTCUT_DOWNLOADS - } - return PendingIntent.getActivity(context, 0, intent, 0) + val intent = Intent(context, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + intent.action = MainActivity.SHORTCUT_DOWNLOADS + return PendingIntent.getActivity(context, -201, intent, PendingIntent.FLAG_UPDATE_CURRENT) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index b72283abec..aff7d10e2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -21,19 +21,18 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.getUriCompat -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.toast +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID - /** * Global [BroadcastReceiver] that runs on UI thread * Pending Broadcasts should be made from here. @@ -113,7 +112,6 @@ class NotificationReceiver : BroadcastReceiver() { type = "image/*" } // Close Navigation Shade - } /** @@ -135,7 +133,7 @@ class NotificationReceiver : BroadcastReceiver() { } context.startActivity(intent) } else { - context.toast(context.getString(R.string.no_next_chapter)) + context.toast(context.getString(R.string.next_chapter_not_found)) } } @@ -217,7 +215,6 @@ class NotificationReceiver : BroadcastReceiver() { // Called to cancel restore private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE" - // Called to open chapter private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER" @@ -322,8 +319,12 @@ class NotificationReceiver : BroadcastReceiver() { * @param notificationId id of notification * @return [PendingIntent] */ - internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? = - null) { + internal fun dismissNotification( + context: Context, + notificationId: Int, + groupId: Int? = + null + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val groupKey = context.notificationManager.activeNotifications.find { it.id == notificationId @@ -350,7 +351,6 @@ class NotificationReceiver : BroadcastReceiver() { * @return [PendingIntent] */ internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent { - //val shareIntent = ShareStartingActivity.newIntent(context, path) val shareIntent = Intent(Intent.ACTION_SEND).apply { val uri = File(path).getUriCompat(context) putExtra(Intent.EXTRA_STREAM, uri) @@ -358,7 +358,6 @@ class NotificationReceiver : BroadcastReceiver() { clipData = ClipData.newRawUri(null, uri) type = "image/*" } - //val shareIntent2 = Intent.createChooser(shareIntent, context.getString(R.string.action_share)) return PendingIntent.getActivity(context, 0, shareIntent, PendingIntent .FLAG_CANCEL_CURRENT) } @@ -387,15 +386,19 @@ class NotificationReceiver : BroadcastReceiver() { * @param manga manga of chapter * @param chapter chapter that needs to be opened */ - internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: - Chapter): PendingIntent { + internal fun openChapterPendingActivity( + context: Context, + manga: Manga, + chapter: + Chapter + ): PendingIntent { val newIntent = ReaderActivity.newIntent(context, manga, chapter) return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent .FLAG_UPDATE_CURRENT) } /** - * Returns [PendingIntent] that opens the manga info controller. + * Returns [PendingIntent] that opens the manga details controller. * * @param context context of application * @param manga manga of chapter @@ -404,8 +407,8 @@ class NotificationReceiver : BroadcastReceiver() { PendingIntent { val newIntent = Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaController.MANGA_EXTRA, manga.id) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + .putExtra(MangaDetailsController.MANGA_EXTRA, manga.id) .putExtra("notificationId", manga.id.hashCode()) .putExtra("groupId", groupId) return PendingIntent.getActivity( @@ -422,7 +425,7 @@ class NotificationReceiver : BroadcastReceiver() { internal fun openExtensionsPendingActivity(context: Context): PendingIntent { val newIntent = Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_EXTENSIONS) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) return PendingIntent.getActivity( context, 0, newIntent, PendingIntent.FLAG_UPDATE_CURRENT ) @@ -440,15 +443,19 @@ class NotificationReceiver : BroadcastReceiver() { return PendingIntent.getActivity(context, 0, toLaunch, 0) } - /** * Returns [PendingIntent] that marks a chapter as read and deletes it if preferred * * @param context context of application * @param manga manga of chapter */ - internal fun markAsReadPendingBroadcast(context: Context, manga: Manga, chapters: - Array, groupId: Int): + internal fun markAsReadPendingBroadcast( + context: Context, + manga: Manga, + chapters: + Array, + groupId: Int + ): PendingIntent { val newIntent = Intent(context, NotificationReceiver::class.java).apply { action = ACTION_MARK_AS_READ @@ -486,4 +493,4 @@ class NotificationReceiver : BroadcastReceiver() { return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index 08ee6201e2..a07bbad751 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -38,6 +38,7 @@ object Notifications { const val CHANNEL_NEW_CHAPTERS = "new_chapters_channel" const val ID_NEW_CHAPTERS = -301 const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS" + /** * Notification channel and ids used by the library updater. */ @@ -49,7 +50,6 @@ object Notifications { const val ID_RESTORE_COMPLETE = -502 const val ID_RESTORE_ERROR = -503 - /** * Creates the notification channels introduced in Android Oreo. * @@ -58,36 +58,37 @@ object Notifications { fun createChannels(context: Context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - val channels = listOf( - NotificationChannel( - CHANNEL_COMMON, - context.getString(R.string.channel_common), - NotificationManager.IMPORTANCE_LOW - ), NotificationChannel( - CHANNEL_LIBRARY, - context.getString(R.string.channel_library_updates), - NotificationManager.IMPORTANCE_LOW - ).apply { - setShowBadge(false) - }, NotificationChannel( - CHANNEL_DOWNLOADER, - context.getString(R.string.channel_downloader), - NotificationManager.IMPORTANCE_LOW - ).apply { - setShowBadge(false) - }, NotificationChannel( - CHANNEL_UPDATES_TO_EXTS, - context.getString(R.string.channel_ext_updates), - NotificationManager.IMPORTANCE_DEFAULT - ), NotificationChannel( - CHANNEL_NEW_CHAPTERS, - context.getString(R.string.channel_new_chapters), - NotificationManager.IMPORTANCE_DEFAULT - ), NotificationChannel(CHANNEL_RESTORE, context.getString(R.string.channel_backup_restore), - NotificationManager.IMPORTANCE_LOW).apply { - setShowBadge(false) - } - ) + val channels = listOf(NotificationChannel( + CHANNEL_COMMON, + context.getString(R.string.common), + NotificationManager.IMPORTANCE_LOW + ), NotificationChannel( + CHANNEL_LIBRARY, + context.getString(R.string.updating_library), + NotificationManager.IMPORTANCE_LOW + ).apply { + setShowBadge(false) + }, NotificationChannel( + CHANNEL_DOWNLOADER, + context.getString(R.string.downloads), + NotificationManager.IMPORTANCE_LOW + ).apply { + setShowBadge(false) + }, NotificationChannel( + CHANNEL_UPDATES_TO_EXTS, + context.getString(R.string.extension_updates), + NotificationManager.IMPORTANCE_DEFAULT + ), NotificationChannel( + CHANNEL_NEW_CHAPTERS, + context.getString(R.string.new_chapters), + NotificationManager.IMPORTANCE_DEFAULT + ), NotificationChannel( + CHANNEL_RESTORE, + context.getString(R.string.restoring_backup), + NotificationManager.IMPORTANCE_LOW + ).apply { + setShowBadge(false) + }) context.notificationManager.createNotificationChannels(channels) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index ea3d9eb08a..1ddd54b2db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -97,7 +97,9 @@ object PreferenceKeys { const val filterCompleted = "pref_filter_completed_key" - const val filterTrcaked = "pref_filter_tracked_key" + const val filterTracked = "pref_filter_tracked_key" + + const val filterMangaType = "pref_filter_manga_type_key" const val librarySortingMode = "library_sorting_mode" @@ -105,13 +107,17 @@ object PreferenceKeys { const val automaticExtUpdates = "automatic_ext_updates" - const val startScreen = "start_screen" - const val downloadNew = "download_new" const val downloadNewCategories = "download_new_categories" - const val libraryAsList = "pref_display_library_as_list" + const val libraryLayout = "pref_display_library_layout" + + const val gridSize = "grid_size" + + const val uniformGrid = "uniform_grid" + + const val libraryAsSingleList = "library_as_single_list" const val lang = "app_language" @@ -129,12 +135,18 @@ object PreferenceKeys { const val lastUnlock = "last_unlock" + const val secureScreen = "secure_screen" + const val removeArticles = "remove_articles" const val skipPreMigration = "skip_pre_migration" const val refreshCoversToo = "refresh_covers_too" + const val updateOnRefresh = "update_on_refresh" + + const val alwaysShowChapterTransition = "always_show_chapter_transition" + @Deprecated("Use the preferences of the source") fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" @@ -148,5 +160,4 @@ object PreferenceKeys { fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackToken(syncId: Int) = "track_token_$syncId" - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index ab2c49be7a..29282ba4cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -8,13 +8,12 @@ import androidx.preference.PreferenceManager import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.RxSharedPreferences import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.source.Source import java.io.File -import java.util.Locale import java.text.DateFormat import java.text.SimpleDateFormat +import java.util.Locale import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys fun Preference.getOrDefault(): T = get() ?: defaultValue()!! @@ -54,7 +53,7 @@ class PreferencesHelper(val context: Context) { fun getStringPref(key: String, default: String?) = rxPrefs.getString(key, default) fun getStringSet(key: String, default: Set) = rxPrefs.getStringSet(key, default) - fun startScreen() = prefs.getInt(Keys.startScreen, 1) + fun lastTab() = rxPrefs.getInteger("last_tab", 0) fun clear() = prefs.edit().clear().apply() @@ -174,7 +173,15 @@ class PreferencesHelper(val context: Context) { fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0) - fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) + fun libraryLayout() = rxPrefs.getInteger(Keys.libraryLayout, 1) + + fun gridSize() = rxPrefs.getInteger(Keys.gridSize, 1) + + fun alwaysShowSeeker() = rxPrefs.getBoolean("always_show_seeker", false) + + fun uniformGrid() = rxPrefs.getBoolean(Keys.uniformGrid, true) + + fun chaptersDescAsDefault() = rxPrefs.getBoolean("chapters_desc_as_default", true) fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false) @@ -184,7 +191,9 @@ class PreferencesHelper(val context: Context) { fun filterCompleted() = rxPrefs.getInteger(Keys.filterCompleted, 0) - fun filterTracked() = rxPrefs.getInteger(Keys.filterTrcaked, 0) + fun filterTracked() = rxPrefs.getInteger(Keys.filterTracked, 0) + + fun filterMangaType() = rxPrefs.getInteger(Keys.filterMangaType, 0) fun hideCategories() = rxPrefs.getBoolean("hide_categories", false) @@ -214,6 +223,8 @@ class PreferencesHelper(val context: Context) { fun lastUnlock() = rxPrefs.getLong(Keys.lastUnlock, 0) + fun secureScreen() = rxPrefs.getBoolean(Keys.secureScreen, false) + fun removeArticles() = rxPrefs.getBoolean(Keys.removeArticles, false) fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE) @@ -230,16 +241,17 @@ class PreferencesHelper(val context: Context) { fun refreshCoversToo() = rxPrefs.getBoolean(Keys.refreshCoversToo, true) + fun updateOnRefresh() = rxPrefs.getInteger(Keys.updateOnRefresh, -1) + fun extensionUpdatesCount() = rxPrefs.getInteger("ext_updates_count", 0) + fun recentsViewType() = rxPrefs.getInteger("recents_view_type", 0) + fun lastExtCheck() = rxPrefs.getLong("last_ext_check", 0) - fun upgradeFilters() { - val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault() - val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault() - val filterCm = rxPrefs.getBoolean(Keys.filterCompleted, false).getOrDefault() - filterDownloaded().set(if (filterDl) 1 else 0) - filterUnread().set(if (filterUn) 1 else 0) - filterCompleted().set(if (filterCm) 1 else 0) - } + fun unreadBadgeType() = rxPrefs.getInteger("unread_badge_type", 2) + + fun hideFiltersAtStart() = rxPrefs.getBoolean("hide_filters_at_start", false) + + fun alwaysShowChapterTransition() = rxPrefs.getBoolean(Keys.alwaysShowChapterTransition, true) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 62c34d422f..173a6ffd6f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.track import android.content.Context import eu.kanade.tachiyomi.data.track.anilist.Anilist +import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.kitsu.Kitsu -import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist +import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.shikimori.Shikimori -import eu.kanade.tachiyomi.data.track.bangumi.Bangumi -class TrackManager(private val context: Context) { +class TrackManager(context: Context) { companion object { const val MYANIMELIST = 1 @@ -17,7 +17,7 @@ class TrackManager(private val context: Context) { const val BANGUMI = 5 } - val myAnimeList = Myanimelist(context, MYANIMELIST) + val myAnimeList = MyAnimeList(context, MYANIMELIST) val aniList = Anilist(context, ANILIST) @@ -32,5 +32,4 @@ class TrackManager(private val context: Context) { fun getService(id: Int) = services.find { it.id == id } fun hasLoggedServices() = services.any { it.isLogged } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt index f4bfcaeff4..8306b849f6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -3,12 +3,10 @@ package eu.kanade.tachiyomi.data.track import androidx.annotation.CallSuper import androidx.annotation.DrawableRes import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.NetworkHelper import okhttp3.OkHttpClient -import rx.Completable -import rx.Observable import uy.kohesive.injekt.injectLazy abstract class TrackService(val id: Int) { @@ -29,6 +27,8 @@ abstract class TrackService(val id: Int) { abstract fun getStatusList(): List + abstract fun isCompletedStatus(index: Int): Boolean + abstract fun getStatus(status: Int): String abstract fun getScoreList(): List @@ -39,17 +39,15 @@ abstract class TrackService(val id: Int) { abstract fun displayScore(track: Track): String - abstract fun add(track: Track): Observable - - abstract fun update(track: Track): Observable + abstract suspend fun update(track: Track): Track - abstract fun bind(track: Track): Observable + abstract suspend fun bind(track: Track): Track - abstract fun search(query: String): Observable> + abstract suspend fun search(query: String): List - abstract fun refresh(track: Track): Observable + abstract suspend fun refresh(track: Track): Track - abstract fun login(username: String, password: String): Completable + abstract suspend fun login(username: String, password: String): Boolean @CallSuper open fun logout() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index c3b8e4e8bc..717e315d62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -7,31 +7,11 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable +import timber.log.Timber import uy.kohesive.injekt.injectLazy class Anilist(private val context: Context, id: Int) : TrackService(id) { - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLANNING = 5 - const val REPEATING = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - - const val POINT_100 = "POINT_100" - const val POINT_10 = "POINT_10" - const val POINT_10_DECIMAL = "POINT_10_DECIMAL" - const val POINT_5 = "POINT_5" - const val POINT_3 = "POINT_3" - } - override val name = "AniList" private val gson: Gson by injectLazy() @@ -52,22 +32,22 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } } - override fun getLogo() = R.drawable.tracker_anilist + override fun getLogo() = R.drawable.ic_tracker_anilist override fun getLogoColor() = Color.rgb(18, 25, 35) - override fun getStatusList(): List { - return listOf(READING, PLANNING, COMPLETED, REPEATING, ON_HOLD, DROPPED) - } + override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED) + + override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED override fun getStatus(status: Int): String = with(context) { when (status) { READING -> getString(R.string.reading) COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.paused) + PAUSED -> getString(R.string.paused) DROPPED -> getString(R.string.dropped) PLANNING -> getString(R.string.plan_to_read) - REPEATING -> getString(R.string.repeating) + REPEATING -> getString(R.string.rereading) else -> "" } } @@ -95,13 +75,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { // 100 point POINT_100 -> index.toFloat() // 5 stars - POINT_5 -> when { - index == 0 -> 0f + POINT_5 -> when (index) { + 0 -> 0f else -> index * 20f - 10f } // Smiley - POINT_3 -> when { - index == 0 -> 0f + POINT_3 -> when (index) { + 0 -> 0f else -> index * 25f + 10f } // 10 point decimal @@ -114,8 +94,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { val score = track.score return when (scorePreference.getOrDefault()) { - POINT_5 -> when { - score == 0f -> "0 ★" + POINT_5 -> when (score) { + 0f -> "0 ★" else -> "${((score + 10) / 20).toInt()} ★" } POINT_3 -> when { @@ -128,68 +108,61 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } } - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { + override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED } // If user was using API v1 fetch library_id - if (track.library_id == null || track.library_id!! == 0L){ - return api.findLibManga(track, getUsername().toInt()).flatMap { - if (it == null) { - throw Exception("$track not found on user library") - } - track.library_id = it.library_id - api.updateLibManga(track) - } + if (track.library_id == null || track.library_id!! == 0L) { + val libManga = api.findLibManga(track, getUsername().toInt()) + ?: throw Exception("$track not found on user library") + + track.library_id = libManga.library_id } - return api.updateLibManga(track) + return api.updateLibraryManga(track) } - override fun bind(track: Track): Observable { - return api.findLibManga(track, getUsername().toInt()) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.library_id = remoteTrack.library_id - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } - } + override suspend fun bind(track: Track): Track { + val remoteTrack = api.findLibManga(track, getUsername().toInt()) - override fun search(query: String): Observable> { - return api.search(query) + return if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + api.addLibManga(track) + } } - override fun refresh(track: Track): Observable { - return api.getLibManga(track, getUsername().toInt()) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } + override suspend fun search(query: String) = api.search(query) + + override suspend fun refresh(track: Track): Track { + val remoteTrack = api.getLibManga(track, getUsername().toInt()) + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + return track } - override fun login(username: String, password: String) = login(password) + override suspend fun login(username: String, password: String) = login(password) - fun login(token: String): Completable { + suspend fun login(token: String): Boolean { val oauth = api.createOAuth(token) interceptor.setAuth(oauth) - return api.getCurrentUser().map { (username, scoreType) -> - scorePreference.set(scoreType) - saveCredentials(username.toString(), oauth.access_token) - }.doOnError{ + + return try { + val currentUser = api.getCurrentUser() + scorePreference.set(currentUser.second) + saveCredentials(currentUser.first.toString(), oauth.access_token) + true + } catch (e: Exception) { + Timber.e(e) logout() - }.toCompletable() + false + } } override fun logout() { @@ -206,9 +179,26 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { return try { gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java) } catch (e: Exception) { + Timber.e(e) null } } -} + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val PAUSED = 3 + const val DROPPED = 4 + const val PLANNING = 5 + const val REPEATING = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + const val POINT_100 = "POINT_100" + const val POINT_10 = "POINT_10" + const val POINT_10_DECIMAL = "POINT_10_DECIMAL" + const val POINT_5 = "POINT_5" + const val POINT_3 = "POINT_3" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 2dacccf141..8473a54c6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -11,25 +11,209 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.jsonType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.MediaType -import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody -import rx.Observable +import okhttp3.Response import java.util.Calendar - +import java.util.concurrent.TimeUnit class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { - private val parser = JsonParser() - private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - fun addLibManga(track: Track): Observable { - val query = """ + suspend fun addLibManga(track: Track): Track { + return withContext(Dispatchers.IO) { + + val variables = jsonObject( + "mangaId" to track.media_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus() + ) + val payload = jsonObject( + "query" to addToLibraryQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + + val netResponse = authClient.newCall(request).execute() + + val responseBody = netResponse.body?.string().orEmpty() + netResponse.close() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = JsonParser.parseString(responseBody).obj + track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong + track + } + } + + suspend fun updateLibraryManga(track: Track): Track { + return withContext(Dispatchers.IO) { + val variables = jsonObject( + "listId" to track.library_id, + "progress" to track.last_chapter_read, + "status" to track.toAnilistStatus(), + "score" to track.score.toInt() + ) + val payload = jsonObject( + "query" to updateInLibraryQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val response = authClient.newCall(request).execute() + + track + } + } + + suspend fun search(search: String): List { + return withContext(Dispatchers.IO) { + val variables = jsonObject( + "query" to search + ) + val payload = jsonObject( + "query" to searchQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val netResponse = authClient.newCall(request).execute() + val response = responseToJson(netResponse) + + val media = response["data"]!!.obj["Page"].obj["media"].array + val entries = media.map { jsonToALManga(it.obj) } + entries.map { it.toTrack() } + } + } + + suspend fun findLibManga(track: Track, userid: Int): Track? { + + return withContext(Dispatchers.IO) { + val variables = jsonObject( + "id" to userid, + "manga_id" to track.media_id + ) + val payload = jsonObject( + "query" to findLibraryMangaQuery(), + "variables" to variables + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val result = authClient.newCall(request).execute() + + result.let { resp -> + val response = responseToJson(resp) + val media = response["data"]!!.obj["Page"].obj["mediaList"].array + val entries = media.map { jsonToALUserManga(it.obj) } + + entries.firstOrNull()?.toTrack() + } + } + } + + suspend fun getLibManga(track: Track, userid: Int): Track { + val remoteTrack = findLibManga(track, userid) + if (remoteTrack == null) { + throw Exception("Could not find manga") + } else { + return remoteTrack + } + } + + fun createOAuth(token: String): OAuth { + return OAuth( + token, + "Bearer", + System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365), + TimeUnit.DAYS.toMillis(365) + ) + } + + suspend fun getCurrentUser(): Pair { + return withContext(Dispatchers.IO) { + val payload = jsonObject( + "query" to currentUserQuery() + ) + val body = payload.toString().toRequestBody(MediaType.jsonType()) + val request = Request.Builder().url(apiUrl).post(body).build() + val netResponse = authClient.newCall(request).execute() + + val response = responseToJson(netResponse) + val viewer = response["data"]!!.obj["Viewer"].obj + + Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) + } + } + + private fun responseToJson(netResponse: Response): JsonObject { + val responseBody = netResponse.body?.string().orEmpty() + + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + + return JsonParser.parseString(responseBody).obj + } + + private fun jsonToALManga(struct: JsonObject): ALManga { + val date = try { + val date = Calendar.getInstance() + date.set( + struct["startDate"]["year"].nullInt ?: 0, + (struct["startDate"]["month"].nullInt ?: 0) - 1, + struct["startDate"]["day"].nullInt ?: 0 + ) + date.timeInMillis + } catch (_: Exception) { + 0L + } + + return ALManga( + struct["id"].asInt, + struct["title"]["romaji"].asString, + struct["coverImage"]["large"].asString, + struct["description"].nullString.orEmpty(), + struct["type"].asString, + struct["status"].asString, + date, + struct["chapters"].nullInt ?: 0 + ) + } + + private fun jsonToALUserManga(struct: JsonObject): ALUserManga { + return ALUserManga( + struct["id"].asLong, + struct["status"].asString, + struct["scoreRaw"].asInt, + struct["progress"].asInt, + jsonToALManga(struct["media"].obj) + ) + } + + companion object { + private const val clientId = "385" + private const val apiUrl = "https://graphql.anilist.co/" + private const val baseUrl = "https://anilist.co/api/v2/" + private const val baseMangaUrl = "https://anilist.co/manga/" + + fun mangaUrl(mediaId: Int): String { + return baseMangaUrl + mediaId + } + + fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("response_type", "token") + .build()!! + + fun addToLibraryQuery() = """ |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { | id @@ -37,36 +221,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "mangaId" to track.media_id, - "progress" to track.last_chapter_read, - "status" to track.toAnilistStatus() - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - netResponse.close() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong - track - } - } - fun updateLibManga(track: Track): Observable { - val query = """ + fun updateInLibraryQuery() = """ |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { |id @@ -75,30 +231,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "listId" to track.library_id, - "progress" to track.last_chapter_read, - "status" to track.toAnilistStatus(), - "score" to track.score.toInt() - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { - track - } - } - fun search(search: String): Observable> { - val query = """ + fun searchQuery() = """ |query Search(${'$'}query: String) { |Page (perPage: 50) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { @@ -122,37 +256,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "query" to search - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - val data = response["data"]!!.obj - val page = data["Page"].obj - val media = page["media"].array - val entries = media.map { jsonToALManga(it.obj) } - entries.map { it.toTrack() } - } - } - - fun findLibManga(track: Track, userid: Int): Observable { - val query = """ + fun findLibraryMangaQuery() = """ |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |Page { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { @@ -182,47 +287,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val variables = jsonObject( - "id" to userid, - "manga_id" to track.media_id - ) - val payload = jsonObject( - "query" to query, - "variables" to variables - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - val data = response["data"]!!.obj - val page = data["Page"].obj - val media = page["mediaList"].array - val entries = media.map { jsonToALUserManga(it.obj) } - entries.firstOrNull()?.toTrack() - - } - } - - fun getLibManga(track: Track, userid: Int): Observable { - return findLibManga(track, userid) - .map { it ?: throw Exception("Could not find manga") } - } - fun createOAuth(token: String): OAuth { - return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) - } - - fun getCurrentUser(): Observable> { - val query = """ + fun currentUserQuery() = """ |query User { |Viewer { |id @@ -232,62 +298,5 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { |} |} |""".trimMargin() - val payload = jsonObject( - "query" to query - ) - val body = payload.toString().toRequestBody(jsonMime) - val request = Request.Builder() - .url(apiUrl) - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).obj - val data = response["data"]!!.obj - val viewer = data["Viewer"].obj - Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) - } - } - - private fun jsonToALManga(struct: JsonObject): ALManga { - val date = try { - val date = Calendar.getInstance() - date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1, - struct["startDate"]["day"].nullInt ?: 0) - date.timeInMillis - } catch (_: Exception) { - 0L - } - - return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, - struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, - date, struct["chapters"].nullInt ?: 0) } - - private fun jsonToALUserManga(struct: JsonObject): ALUserManga { - return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj)) - } - - companion object { - private const val clientId = "385" - private const val clientUrl = "tachiyomi://anilist-auth" - private const val apiUrl = "https://graphql.anilist.co/" - private const val baseUrl = "https://anilist.co/api/v2/" - private const val baseMangaUrl = "https://anilist.co/manga/" - - fun mangaUrl(mediaId: Int): String { - return baseMangaUrl + mediaId - } - - fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("response_type", "token") - .build() - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index ff416a1c5f..8c9e33bf89 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -3,8 +3,7 @@ package eu.kanade.tachiyomi.data.track.anilist import okhttp3.Interceptor import okhttp3.Response - -class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { +class AnilistInterceptor(private val anilist: Anilist, private var token: String?) : Interceptor { /** * OAuth object used for authenticated requests. @@ -23,7 +22,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int if (token.isNullOrEmpty()) { throw Exception("Not authenticated with Anilist") } - if (oauth == null){ + if (oauth == null) { oauth = anilist.loadOAuth() } // Refresh access token if null or expired. @@ -54,5 +53,4 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int this.oauth = oauth anilist.saveOAuth(oauth) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 8f96723d08..c94ae4b0c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -7,17 +7,28 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat -import java.util.* +import java.util.Locale + +data class OAuth( + val access_token: String, + val token_type: String, + val expires: Long, + val expires_in: Long +) { + + fun isExpired() = System.currentTimeMillis() > expires +} data class ALManga( - val media_id: Int, - val title_romaji: String, - val image_url_lge: String, - val description: String?, - val type: String, - val publishing_status: String, - val start_date_fuzzy: Long, - val total_chapters: Int) { + val media_id: Int, + val title_romaji: String, + val image_url_lge: String, + val description: String?, + val type: String, + val publishing_status: String, + val start_date_fuzzy: Long, + val total_chapters: Int +) { fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply { media_id = this@ALManga.media_id @@ -40,11 +51,12 @@ data class ALManga( } data class ALUserManga( - val library_id: Long, - val list_status: String, - val score_raw: Int, - val chapters_read: Int, - val manga: ALManga) { + val library_id: Long, + val list_status: String, + val score_raw: Int, + val chapters_read: Int, + val manga: ALManga +) { fun toTrack() = Track.create(TrackManager.ANILIST).apply { media_id = manga.media_id @@ -55,10 +67,10 @@ data class ALUserManga( total_chapters = manga.total_chapters } - fun toTrackStatus() = when (list_status) { + private fun toTrackStatus() = when (list_status) { "CURRENT" -> Anilist.READING "COMPLETED" -> Anilist.COMPLETED - "PAUSED" -> Anilist.ON_HOLD + "PAUSED" -> Anilist.PAUSED "DROPPED" -> Anilist.DROPPED "PLANNING" -> Anilist.PLANNING "REPEATING" -> Anilist.REPEATING @@ -69,7 +81,7 @@ data class ALUserManga( fun Track.toAnilistStatus() = when (status) { Anilist.READING -> "CURRENT" Anilist.COMPLETED -> "COMPLETED" - Anilist.ON_HOLD -> "PAUSED" + Anilist.PAUSED -> "PAUSED" Anilist.DROPPED -> "DROPPED" Anilist.PLANNING -> "PLANNING" Anilist.REPEATING -> "REPEATING" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt deleted file mode 100644 index a53760ba5d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt +++ /dev/null @@ -1,10 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist - -data class OAuth( - val access_token: String, - val token_type: String, - val expires: Long, - val expires_in: Long) { - - fun isExpired() = System.currentTimeMillis() > expires -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Avatar.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Avatar.kt deleted file mode 100644 index d058a85f59..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Avatar.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class Avatar( - val large: String? = "", - val medium: String? = "", - val small: String? = "" -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 147dde6de6..3bc3231ec5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable +import timber.log.Timber import uy.kohesive.injekt.injectLazy class Bangumi(private val context: Context, id: Int) : TrackService(id) { @@ -29,65 +28,56 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { return track.score.toInt().toString() } - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { + override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED } return api.updateLibManga(track) } - override fun bind(track: Track): Observable { - return api.statusLibManga(track) - .flatMap { - api.findLibManga(track).flatMap { remoteTrack -> - if (remoteTrack != null && it != null) { - track.copyPersonalFrom(remoteTrack) - track.library_id = remoteTrack.library_id - track.status = remoteTrack.status - track.last_chapter_read = remoteTrack.last_chapter_read - refresh(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - update(track) - } - } - } + override suspend fun bind(track: Track): Track { + val statusTrack = api.statusLibManga(track) + val remoteTrack = api.findLibManga(track) + if (statusTrack != null && remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id + track.status = remoteTrack.status + track.last_chapter_read = remoteTrack.last_chapter_read + refresh(track) + } else { + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + api.addLibManga(track) + update(track) + } + return track } - override fun search(query: String): Observable> { + override suspend fun search(query: String): List { return api.search(query) } - override fun refresh(track: Track): Observable { - return api.statusLibManga(track) - .flatMap { - track.copyPersonalFrom(it!!) - api.findLibManga(track) - .map { remoteTrack -> - if (remoteTrack != null) { - track.total_chapters = remoteTrack.total_chapters - track.status = remoteTrack.status - } - track - } - } + override suspend fun refresh(track: Track): Track { + val statusTrack = api.statusLibManga(track) + track.copyPersonalFrom(statusTrack!!) + val remoteTrack = api.findLibManga(track) + if (remoteTrack != null) { + track.total_chapters = remoteTrack.total_chapters + track.status = remoteTrack.status + } + return track } - override fun getLogo() = R.drawable.tracker_bangumi + override fun getLogo() = R.drawable.ic_tracker_bangumi - override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99) + override fun getLogoColor() = Color.rgb(240, 145, 153) override fun getStatusList(): List { return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING) } + override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED + override fun getStatus(status: Int): String = with(context) { when (status) { READING -> getString(R.string.reading) @@ -99,17 +89,20 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { } } - override fun login(username: String, password: String) = login(password) + override suspend fun login(username: String, password: String): Boolean = login(password) + + suspend fun login(code: String): Boolean { + try { - fun login(code: String): Completable { - return api.accessToken(code).map { oauth: OAuth? -> + val oauth = api.accessToken(code) interceptor.newAuth(oauth) - if (oauth != null) { - saveCredentials(oauth.user_id.toString(), oauth.access_token) - } - }.doOnError { + saveCredentials(oauth.user_id.toString(), oauth.access_token) + return true + } catch (e: Exception) { + Timber.e(e) logout() - }.toCompletable() + } + return false } fun saveToken(oauth: OAuth?) { @@ -128,15 +121,15 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) { override fun logout() { super.logout() preferences.trackToken(this).set(null) - interceptor.newAuth(null) + interceptor.clearOauth() } companion object { - const val READING = 3 + const val PLANNING = 1 const val COMPLETED = 2 + const val READING = 3 const val ON_HOLD = 4 const val DROPPED = 5 - const val PLANNING = 1 const val DEFAULT_STATUS = READING const val DEFAULT_SCORE = 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 3b356f6533..3a60e60f2c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -10,91 +10,72 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.await +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.CacheControl import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request -import rx.Observable import uy.kohesive.injekt.injectLazy import java.net.URLEncoder class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) { private val gson: Gson by injectLazy() - private val parser = JsonParser() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - fun addLibManga(track: Track): Observable { - val body = FormBody.Builder() - .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) - .build() - val request = Request.Builder() - .url("$apiUrl/collection/${track.media_id}/update") - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { - track - } + suspend fun addLibManga(track: Track): Track { + val body = FormBody.Builder().add("rating", track.score.toInt().toString()) + .add("status", track.toBangumiStatus()).build() + val request = + Request.Builder().url("$apiUrl/collection/${track.media_id}/update").post(body).build() + val response = authClient.newCall(request).await() + return track } - fun updateLibManga(track: Track): Observable { + suspend fun updateLibManga(track: Track): Track { // chapter update - val body = FormBody.Builder() - .add("watched_eps", track.last_chapter_read.toString()) - .build() - val request = Request.Builder() - .url("$apiUrl/subject/${track.media_id}/update/watched_eps") - .post(body) - .build() - - // read status update - val sbody = FormBody.Builder() - .add("status", track.toBangumiStatus()) - .build() - val srequest = Request.Builder() - .url("$apiUrl/collection/${track.media_id}/update") - .post(sbody) - .build() - return authClient.newCall(srequest) - .asObservableSuccess() - .map { - track - }.flatMap { - authClient.newCall(request) - .asObservableSuccess() - .map { - track - } - } + return withContext(Dispatchers.IO) { + val body = + FormBody.Builder().add("watched_eps", track.last_chapter_read.toString()).build() + val request = + Request.Builder().url("$apiUrl/subject/${track.media_id}/update/watched_eps") + .post(body).build() + + // read status update + val sbody = FormBody.Builder().add("status", track.toBangumiStatus()).build() + val srequest = + Request.Builder().url("$apiUrl/collection/${track.media_id}/update").post(sbody) + .build() + authClient.newCall(srequest).execute() + authClient.newCall(request).execute() + track + } } - fun search(search: String): Observable> { - val url = Uri.parse( - "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon() - .appendQueryParameter("max_results", "20") - .build() - val request = Request.Builder() - .url(url.toString()) - .get() - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - var responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":404")) { - responseBody = "{\"results\":0,\"list\":[]}" - } - val response = parser.parse(responseBody).obj["list"]?.array - response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } - } + suspend fun search(search: String): List { + return withContext(Dispatchers.IO) { + val url = Uri.parse( + "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}" + ).buildUpon().appendQueryParameter("max_results", "20").build() + val request = Request.Builder().url(url.toString()).get().build() + val netResponse = authClient.newCall(request).await() + var responseBody = netResponse.body?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + if (responseBody.contains("\"code\":404")) { + responseBody = "{\"results\":0,\"list\":[]}" + } + val response = JsonParser.parseString(responseBody).obj["list"]?.array + if (response != null) { + response.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) } + } else { + listOf() + } + } } private fun jsonToSearch(obj: JsonObject): TrackSearch { @@ -111,52 +92,42 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept return Track.create(TrackManager.BANGUMI).apply { title = mangas["name"].asString media_id = mangas["id"].asInt - score = if (mangas["rating"] != null) - (if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f) - else 0f + score = + if (mangas["rating"] != null) (if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f) + else 0f status = Bangumi.DEFAULT_STATUS tracking_url = mangas["url"].asString } } - fun findLibManga(track: Track): Observable { - val urlMangas = "$apiUrl/subject/${track.media_id}" - val requestMangas = Request.Builder() - .url(urlMangas) - .get() - .build() - - return authClient.newCall(requestMangas) - .asObservableSuccess() - .map { netResponse -> - // get comic info - val responseBody = netResponse.body?.string().orEmpty() - jsonToTrack(parser.parse(responseBody).obj) - } + suspend fun findLibManga(track: Track): Track? { + return withContext(Dispatchers.IO) { + val urlMangas = "$apiUrl/subject/${track.media_id}" + val requestMangas = Request.Builder().url(urlMangas).get().build() + val netResponse = authClient.newCall(requestMangas).execute() + val responseBody = netResponse.body?.string().orEmpty() + jsonToTrack(JsonParser.parseString(responseBody).obj) + } } - fun statusLibManga(track: Track): Observable { + suspend fun statusLibManga(track: Track): Track? { val urlUserRead = "$apiUrl/collection/${track.media_id}" - val requestUserRead = Request.Builder() - .url(urlUserRead) - .cacheControl(CacheControl.FORCE_NETWORK) - .get() + val requestUserRead = + Request.Builder().url(urlUserRead).cacheControl(CacheControl.FORCE_NETWORK).get() .build() // todo get user readed chapter here - return authClient.newCall(requestUserRead) - .asObservableSuccess() - .map { netResponse -> - val resp = netResponse.body?.string() - val coll = gson.fromJson(resp, Collection::class.java) - track.status = coll.status?.id!! - track.last_chapter_read = coll.ep_status!! - track - } + val response = authClient.newCall(requestUserRead).await() + val resp = response.body?.toString() + val coll = gson.fromJson(resp, Collection::class.java) + track.status = coll.status?.id!! + track.last_chapter_read = coll.ep_status!! + return track } - fun accessToken(code: String): Observable { - return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse -> + suspend fun accessToken(code: String): OAuth { + return withContext(Dispatchers.IO) { + val netResponse = client.newCall(accessTokenRequest(code)).execute() val responseBody = netResponse.body?.string().orEmpty() if (responseBody.isEmpty()) { throw Exception("Null Response") @@ -165,14 +136,11 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept } } - private fun accessTokenRequest(code: String) = POST(oauthUrl, - body = FormBody.Builder() - .add("grant_type", "authorization_code") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("code", code) - .add("redirect_uri", redirectUrl) - .build() + private fun accessTokenRequest(code: String) = POST( + oauthUrl, + body = FormBody.Builder().add("grant_type", "authorization_code").add("client_id", clientId) + .add("client_secret", clientSecret).add("code", code).add("redirect_uri", redirectUrl) + .build() ) companion object { @@ -191,21 +159,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept return "$baseMangaUrl/$remoteId" } - fun authUrl() = - Uri.parse(loginUrl).buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("response_type", "code") - .appendQueryParameter("redirect_uri", redirectUrl) - .build() - - fun refreshTokenRequest(token: String) = POST(oauthUrl, - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .add("redirect_uri", redirectUrl) - .build()) - } + fun authUrl() = Uri.parse(loginUrl).buildUpon().appendQueryParameter("client_id", clientId) + .appendQueryParameter("response_type", "code") + .appendQueryParameter("redirect_uri", redirectUrl).build() + fun refreshTokenRequest(token: String) = POST( + oauthUrl, + body = FormBody.Builder().add("grant_type", "refresh_token").add("client_id", clientId) + .add("client_secret", clientSecret).add("refresh_token", token) + .add("redirect_uri", redirectUrl).build() + ) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt index add168201e..261f997524 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt @@ -47,8 +47,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor { return chain.proceed(authRequest) } - fun newAuth(oauth: OAuth?) { - this.oauth = if (oauth == null) null else OAuth( + fun newAuth(oauth: OAuth) { + this.oauth = OAuth( oauth.access_token, oauth.token_type, System.currentTimeMillis() / 1000, @@ -58,4 +58,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor { bangumi.saveToken(oauth) } + + fun clearOauth() { + bangumi.saveToken(null) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt index 03143d19f5..5045677e9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Collection.kt @@ -1,13 +1,47 @@ package eu.kanade.tachiyomi.data.track.bangumi data class Collection( - val `private`: Int? = 0, - val comment: String? = "", - val ep_status: Int? = 0, - val lasttouch: Int? = 0, - val rating: Int? = 0, - val status: Status? = Status(), - val tag: List? = listOf(), - val user: User? = User(), - val vol_status: Int? = 0 + val `private`: Int? = 0, + val comment: String? = "", + val ep_status: Int? = 0, + val lasttouch: Int? = 0, + val rating: Int? = 0, + val status: Status? = Status(), + val tag: List? = listOf(), + val user: User? = User(), + val vol_status: Int? = 0 +) + +data class OAuth( + val access_token: String, + val token_type: String, + val created_at: Long, + val expires_in: Long, + val refresh_token: String?, + val user_id: Long? +) { + // Access token refresh before expired + fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) +} + +data class Status( + val id: Int? = 0, + val name: String? = "", + val type: String? = "" +) + +data class User( + val avatar: Avatar? = Avatar(), + val id: Int? = 0, + val nickname: String? = "", + val sign: String? = "", + val url: String? = "", + val usergroup: Int? = 0, + val username: String? = "" +) + +data class Avatar( + val large: String? = "", + val medium: String? = "", + val small: String? = "" ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt deleted file mode 100644 index 811d0fd459..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/OAuth.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?, - val user_id: Long? -) { - - // Access token refresh before expired - fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) - -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Status.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Status.kt deleted file mode 100644 index 3d2ea3c14b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Status.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class Status( - val id: Int? = 0, - val name: String? = "", - val type: String? = "" -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/User.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/User.kt deleted file mode 100644 index 9e82f533e3..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/User.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -data class User( - val avatar: Avatar? = Avatar(), - val id: Int? = 0, - val nickname: String? = "", - val sign: String? = "", - val url: String? = "", - val usergroup: Int? = 0, - val username: String? = "" -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 4b4f8cbcfe..c73999fe96 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable +import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat @@ -34,7 +33,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { private val api by lazy { KitsuApi(client, interceptor) } override fun getLogo(): Int { - return R.drawable.tracker_kitsu + return R.drawable.ic_tracker_kitsu } override fun getLogoColor(): Int { @@ -45,6 +44,8 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { return listOf(READING, PLAN_TO_READ, COMPLETED, ON_HOLD, DROPPED) } + override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED + override fun getStatus(status: Int): String = with(context) { when (status) { READING -> getString(R.string.currently_reading) @@ -70,11 +71,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { return df.format(track.score) } - override fun add(track: Track): Observable { - return api.addLibManga(track, getUserId()) - } - - override fun update(track: Track): Observable { + override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED } @@ -82,41 +79,41 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { return api.updateLibManga(track) } - override fun bind(track: Track): Observable { - return api.findLibManga(track, getUserId()) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.media_id = remoteTrack.media_id - update(track) - } else { - track.score = DEFAULT_SCORE - track.status = DEFAULT_STATUS - add(track) - } - } + override suspend fun bind(track: Track): Track { + val remoteTrack = api.findLibManga(track, getUserId()) + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.media_id = remoteTrack.media_id + return update(track) + } else { + track.score = DEFAULT_SCORE + track.status = DEFAULT_STATUS + return api.addLibManga(track, getUserId()) + } } - override fun search(query: String): Observable> { + override suspend fun search(query: String): List { return api.search(query) } - override fun refresh(track: Track): Observable { - return api.getLibManga(track) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } + override suspend fun refresh(track: Track): Track { + val remoteTrack = api.getLibManga(track) + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + return track } - override fun login(username: String, password: String): Completable { - return api.login(username, password) - .doOnNext { interceptor.newAuth(it) } - .flatMap { api.getCurrentUser() } - .doOnNext { userId -> saveCredentials(username, userId) } - .doOnError { logout() } - .toCompletable() + override suspend fun login(username: String, password: String): Boolean { + try { + val oauth = api.login(username, password) + interceptor.newAuth(oauth) + val userId = api.getCurrentUser() + saveCredentials(username, userId) + return true + } catch (e: Exception) { + Timber.e(e) + return false + } } override fun logout() { @@ -140,5 +137,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { null } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index fa72b6d547..76aad53386 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -1,6 +1,11 @@ package eu.kanade.tachiyomi.data.track.kitsu -import com.github.salomonbrys.kotson.* +import com.github.salomonbrys.kotson.array +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.int +import com.github.salomonbrys.kotson.jsonObject +import com.github.salomonbrys.kotson.obj +import com.github.salomonbrys.kotson.string import com.google.gson.GsonBuilder import com.google.gson.JsonObject import eu.kanade.tachiyomi.data.database.models.Track @@ -9,240 +14,228 @@ import eu.kanade.tachiyomi.network.POST import okhttp3.FormBody import okhttp3.OkHttpClient import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.* -import rx.Observable +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val rest = Retrofit.Builder() - .baseUrl(baseUrl) - .client(authClient) - .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create())) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(KitsuApi.Rest::class.java) + .baseUrl(baseUrl) + .client(authClient) + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create())) + .build() + .create(KitsuApi.Rest::class.java) private val searchRest = Retrofit.Builder() - .baseUrl(algoliaKeyUrl) - .client(authClient) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(KitsuApi.SearchKeyRest::class.java) + .baseUrl(algoliaKeyUrl) + .client(authClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(KitsuApi.SearchKeyRest::class.java) private val algoliaRest = Retrofit.Builder() - .baseUrl(algoliaUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(KitsuApi.AgoliaSearchRest::class.java) - - fun addLibManga(track: Track, userId: String): Observable { - return Observable.defer { - // @formatter:off - val data = jsonObject( - "type" to "libraryEntries", - "attributes" to jsonObject( - "status" to track.toKitsuStatus(), - "progress" to track.last_chapter_read - ), - "relationships" to jsonObject( - "user" to jsonObject( - "data" to jsonObject( - "id" to userId, - "type" to "users" - ) - ), - "media" to jsonObject( - "data" to jsonObject( - "id" to track.media_id, - "type" to "manga" - ) - ) + .baseUrl(algoliaUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(KitsuApi.AgoliaSearchRest::class.java) + + suspend fun addLibManga(track: Track, userId: String): Track { + // @formatter:off + val data = jsonObject( + "type" to "libraryEntries", + "attributes" to jsonObject( + "status" to track.toKitsuStatus(), + "progress" to track.last_chapter_read + ), + "relationships" to jsonObject( + "user" to jsonObject( + "data" to jsonObject( + "id" to userId, + "type" to "users" + ) + ), + "media" to jsonObject( + "data" to jsonObject( + "id" to track.media_id, + "type" to "manga" ) + ) ) + ) - rest.addLibManga(jsonObject("data" to data)) - .map { json -> - track.media_id = json["data"]["id"].int - track - } - } + val json = rest.addLibManga(jsonObject("data" to data)) + track.media_id = json["data"]["id"].int + return track } - fun updateLibManga(track: Track): Observable { - return Observable.defer { - // @formatter:off - val data = jsonObject( - "type" to "libraryEntries", - "id" to track.media_id, - "attributes" to jsonObject( - "status" to track.toKitsuStatus(), - "progress" to track.last_chapter_read, - "ratingTwenty" to track.toKitsuScore() - ) + suspend fun updateLibManga(track: Track): Track { + // @formatter:off + val data = jsonObject( + "type" to "libraryEntries", + "id" to track.media_id, + "attributes" to jsonObject( + "status" to track.toKitsuStatus(), + "progress" to track.last_chapter_read, + "ratingTwenty" to track.toKitsuScore() ) - // @formatter:on + ) + // @formatter:on - rest.updateLibManga(track.media_id, jsonObject("data" to data)) - .map { track } - } + rest.updateLibManga(track.media_id, jsonObject("data" to data)) + return track } - - fun search(query: String): Observable> { - return searchRest - .getKey().map { json -> - json["media"].asJsonObject["key"].string - }.flatMap { key -> - algoliaSearch(key, query) - } + suspend fun search(query: String): List { + val key = searchRest.getKey()["media"].asJsonObject["key"].string + return algoliaSearch(key, query) } - - private fun algoliaSearch(key: String, query: String): Observable> { + private suspend fun algoliaSearch(key: String, query: String): List { val jsonObject = jsonObject("params" to "query=$query$algoliaFilter") - return algoliaRest - .getSearchQuery(algoliaAppId, key, jsonObject) - .map { json -> - val data = json["hits"].array - data.map { KitsuSearchManga(it.obj) } - .filter { it.subType != "novel" } - .map { it.toTrack() } - } + val json = algoliaRest.getSearchQuery(algoliaAppId, key, jsonObject) + val data = json["hits"].array + return data.map { KitsuSearchManga(it.obj) } + .filter { it.subType != "novel" } + .map { it.toTrack() } } - fun findLibManga(track: Track, userId: String): Observable { - return rest.findLibManga(track.media_id, userId) - .map { json -> - val data = json["data"].array - if (data.size() > 0) { - val manga = json["included"].array[0].obj - KitsuLibManga(data[0].obj, manga).toTrack() - } else { - null - } - } + suspend fun findLibManga(track: Track, userId: String): Track? { + val json = rest.findLibManga(track.media_id, userId) + val data = json["data"].array + return if (data.size() > 0) { + val manga = json["included"].array[0].obj + KitsuLibManga(data[0].obj, manga).toTrack() + } else { + null + } } - fun getLibManga(track: Track): Observable { - return rest.getLibManga(track.media_id) - .map { json -> - val data = json["data"].array - if (data.size() > 0) { - val manga = json["included"].array[0].obj - KitsuLibManga(data[0].obj, manga).toTrack() - } else { - throw Exception("Could not find manga") - } - } + suspend fun getLibManga(track: Track): Track { + val json = rest.getLibManga(track.media_id) + val data = json["data"].array + if (data.size() > 0) { + val manga = json["included"].array[0].obj + return KitsuLibManga(data[0].obj, manga).toTrack() + } else { + throw Exception("Could not find manga") + } } - fun login(username: String, password: String): Observable { + suspend fun login(username: String, password: String): OAuth { return Retrofit.Builder() - .baseUrl(loginUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(KitsuApi.LoginRest::class.java) - .requestAccessToken(username, password) + .baseUrl(loginUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(KitsuApi.LoginRest::class.java) + .requestAccessToken(username, password) } - fun getCurrentUser(): Observable { - return rest.getCurrentUser().map { it["data"].array[0]["id"].string } + suspend fun getCurrentUser(): String { + val currentUser = rest.getCurrentUser() + return currentUser["data"].array[0]["id"].string } private interface Rest { @Headers("Content-Type: application/vnd.api+json") @POST("library-entries") - fun addLibManga( - @Body data: JsonObject - ): Observable + suspend fun addLibManga( + @Body data: JsonObject + ): JsonObject @Headers("Content-Type: application/vnd.api+json") @PATCH("library-entries/{id}") - fun updateLibManga( - @Path("id") remoteId: Int, - @Body data: JsonObject - ): Observable - + suspend fun updateLibManga( + @Path("id") remoteId: Int, + @Body data: JsonObject + ): JsonObject @GET("library-entries") - fun findLibManga( - @Query("filter[manga_id]", encoded = true) remoteId: Int, - @Query("filter[user_id]", encoded = true) userId: String, - @Query("include") includes: String = "manga" - ): Observable + suspend fun findLibManga( + @Query("filter[manga_id]", encoded = true) remoteId: Int, + @Query("filter[user_id]", encoded = true) userId: String, + @Query("include") includes: String = "manga" + ): JsonObject @GET("library-entries") - fun getLibManga( - @Query("filter[id]", encoded = true) remoteId: Int, - @Query("include") includes: String = "manga" - ): Observable + suspend fun getLibManga( + @Query("filter[id]", encoded = true) remoteId: Int, + @Query("include") includes: String = "manga" + ): JsonObject @GET("users") - fun getCurrentUser( - @Query("filter[self]", encoded = true) self: Boolean = true - ): Observable - + suspend fun getCurrentUser( + @Query("filter[self]", encoded = true) self: Boolean = true + ): JsonObject } private interface SearchKeyRest { @GET("media/") - fun getKey(): Observable + suspend fun getKey(): JsonObject } private interface AgoliaSearchRest { @POST("query/") - fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable + suspend fun getSearchQuery( + @Header("X-Algolia-Application-Id") appid: String, + @Header("X-Algolia-API-Key") key: String, + @Body json: JsonObject + ): JsonObject } private interface LoginRest { @FormUrlEncoded @POST("oauth/token") - fun requestAccessToken( - @Field("username") username: String, - @Field("password") password: String, - @Field("grant_type") grantType: String = "password", - @Field("client_id") client_id: String = clientId, - @Field("client_secret") client_secret: String = clientSecret - ): Observable - + suspend fun requestAccessToken( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grantType: String = "password", + @Field("client_id") client_id: String = clientId, + @Field("client_secret") client_secret: String = clientSecret + ): OAuth } companion object { - private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" - private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" + private const val clientId = + "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" + private const val clientSecret = + "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" private const val baseUrl = "https://kitsu.io/api/edge/" private const val loginUrl = "https://kitsu.io/api/" private const val baseMangaUrl = "https://kitsu.io/manga/" private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/" - private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/" + private const val algoliaUrl = + "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/" private const val algoliaAppId = "AWQO5J657S" - private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" - + private const val algoliaFilter = + "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" fun mangaUrl(remoteId: Int): String { return baseMangaUrl + remoteId } - - fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token", - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .build()) - + fun refreshTokenRequest(token: String) = POST( + "${loginUrl}oauth/token", + body = FormBody.Builder() + .add("grant_type", "refresh_token") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("refresh_token", token) + .build() + ) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt index 44b874bea7..1f1d4e8f2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt @@ -42,5 +42,4 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor { this.oauth = oauth kitsu.saveToken(oauth) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index 5e709e810f..9664d87924 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -1,13 +1,19 @@ package eu.kanade.tachiyomi.data.track.kitsu import androidx.annotation.CallSuper -import com.github.salomonbrys.kotson.* +import com.github.salomonbrys.kotson.byInt +import com.github.salomonbrys.kotson.byString +import com.github.salomonbrys.kotson.nullInt +import com.github.salomonbrys.kotson.nullObj +import com.github.salomonbrys.kotson.nullString +import com.github.salomonbrys.kotson.obj import com.google.gson.JsonObject import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale class KitsuSearchManga(obj: JsonObject) { val id by obj.byInt @@ -40,7 +46,6 @@ class KitsuSearchManga(obj: JsonObject) { } } - class KitsuLibManga(obj: JsonObject, manga: JsonObject) { val id by manga.byInt private val canonicalTitle by manga["attributes"].byString @@ -77,7 +82,6 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) { "planned" -> Kitsu.PLAN_TO_READ else -> throw Exception("Unknown status") } - } fun Track.toKitsuStatus() = when (status) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt index 678567ce99..a10981c51e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt @@ -1,11 +1,12 @@ package eu.kanade.tachiyomi.data.track.kitsu data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?) { + val access_token: String, + val token_type: String, + val created_at: Long, + val expires_in: Long, + val refresh_token: String? +) { fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt index a7fb8b80d2..fe8bb99aef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -58,7 +58,5 @@ class TrackSearch : Track { fun create(serviceId: Int): TrackSearch = TrackSearch().apply { sync_id = serviceId } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index db14187fc0..0fe9b2484a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -7,35 +7,17 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch -import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import rx.Completable -import rx.Observable +import timber.log.Timber -class Myanimelist(private val context: Context, id: Int) : TrackService(id) { - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLAN_TO_READ = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - - const val BASE_URL = "https://myanimelist.net" - const val USER_SESSION_COOKIE = "MALSESSIONID" - const val LOGGED_IN_COOKIE = "is_logged_in" - } +class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { private val interceptor by lazy { MyAnimeListInterceptor(this) } private val api by lazy { MyAnimeListApi(client, interceptor) } - override val name: String - get() = "MyAnimeList" + override val name = "MyAnimeList" - override fun getLogo() = R.drawable.tracker_mal + override fun getLogo() = R.drawable.ic_tracker_mal override fun getLogoColor() = Color.rgb(46, 81, 162) @@ -54,6 +36,8 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) } + override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED + override fun getScoreList(): List { return IntRange(0, 10).map(Int::toString) } @@ -62,11 +46,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { return track.score.toInt().toString() } - override fun add(track: Track): Observable { - return api.addLibManga(track) - } - - override fun update(track: Track): Observable { + override suspend fun update(track: Track): Track { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED } @@ -74,45 +54,46 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { return api.updateLibManga(track) } - override fun bind(track: Track): Observable { - return api.findLibManga(track) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } + override suspend fun bind(track: Track): Track { + val remoteTrack = api.findLibManga(track) + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + return api.addLibManga(track) + } + return track } - override fun search(query: String): Observable> { + override suspend fun search(query: String): List { return api.search(query) } - override fun refresh(track: Track): Observable { - return api.getLibManga(track) - .map { remoteTrack -> - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } + override suspend fun refresh(track: Track): Track { + val remoteTrack = api.getLibManga(track) + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + return track } - override fun login(username: String, password: String): Completable { + override suspend fun login(username: String, password: String): Boolean { logout() - - return Observable.fromCallable { api.login(username, password) } - .doOnNext { csrf -> saveCSRF(csrf) } - .doOnNext { saveCredentials(username, password) } - .doOnError { logout() } - .toCompletable() + return try { + val csrf = api.login(username, password) + saveCSRF(csrf) + saveCredentials(username, password) + true + } catch (e: Exception) { + Timber.e(e) + logout() + false + } } - fun refreshLogin() { + private suspend fun refreshLogin() { val username = getUsername() val password = getPassword() logout() @@ -122,13 +103,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { saveCSRF(csrf) saveCredentials(username, password) } catch (e: Exception) { + Timber.e(e) logout() throw e } } // Attempt to login again if cookies have been cleared but credentials are still filled - fun ensureLoggedIn() { + suspend fun ensureLoggedIn() { if (isAuthorized) return if (!isLogged) throw Exception("MAL Login Credentials not found") @@ -141,10 +123,8 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) } - val isAuthorized: Boolean - get() = super.isLogged && - getCSRF().isNotEmpty() && - checkCookies() + private val isAuthorized: Boolean + get() = super.isLogged && getCSRF().isNotEmpty() && checkCookies() fun getCSRF(): String = preferences.trackToken(this).getOrDefault() @@ -154,11 +134,24 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { var ckCount = 0 val url = BASE_URL.toHttpUrlOrNull()!! for (ck in networkService.cookieManager.get(url)) { - if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) - ckCount++ + if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) ckCount++ } return ckCount == 2 } + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLAN_TO_READ = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + + const val BASE_URL = "https://myanimelist.net" + const val USER_SESSION_COOKIE = "MALSESSIONID" + const val LOGGED_IN_COOKIE = "is_logged_in" + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 65c16d0e25..af94d88eb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -6,51 +6,40 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservable -import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.network.consumeBody +import eu.kanade.tachiyomi.network.consumeXmlBody import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.FormBody import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response import org.json.JSONObject import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.jsoup.parser.Parser -import rx.Observable -import java.io.BufferedReader -import java.io.InputStreamReader -import java.util.zip.GZIPInputStream - class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { private val authClient = client.newBuilder().addInterceptor(interceptor).build() - fun search(query: String): Observable> { - return if (query.startsWith(PREFIX_MY)) { - val realQuery = query.removePrefix(PREFIX_MY) - getList() - .flatMap { Observable.from(it) } - .filter { it.title.contains(realQuery, true) } - .toList() - } else { - client.newCall(GET(searchUrl(query))) - .asObservable() - .flatMap { response -> - Observable.from(Jsoup.parse(response.consumeBody()) - .select("div.js-categories-seasonal.js-block-list.list") - .select("table").select("tbody") - .select("tr").drop(1)) - } - .filter { row -> - row.select(TD)[2].text() != "Novel" - } - .map { row -> + suspend fun search(query: String): List { + return withContext(Dispatchers.IO) { + if (query.startsWith(PREFIX_MY)) { + queryUsersList(query) + } else { + val realQuery = query.take(100) + val response = client.newCall(GET(searchUrl(realQuery))).await() + val matches = Jsoup.parse(response.consumeBody()) + .select("div.js-categories-seasonal.js-block-list.list").select("table") + .select("tbody").select("tr").drop(1) + + matches.filter { row -> row.select(TD)[2].text() != "Novel" }.map { row -> TrackSearch.create(TrackManager.MYANIMELIST).apply { title = row.searchTitle() media_id = row.searchMediaId() @@ -62,136 +51,113 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI publishing_type = row.searchPublishingType() start_date = row.searchStartDate() } - } - .toList() + }.toList() + } } } - fun addLibManga(track: Track): Observable { - return Observable.defer { - authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))) - .asObservableSuccess() - .map { track } - } + private suspend fun queryUsersList(query: String): List { + val realQuery = query.removePrefix(PREFIX_MY).take(100) + return getList().filter { it.title.contains(realQuery, true) }.toList() } - fun updateLibManga(track: Track): Observable { - return Observable.defer { - authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))) - .asObservableSuccess() - .map { track } - } + suspend fun addLibManga(track: Track): Track { + authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await() + return track } - fun findLibManga(track: Track): Observable { - return authClient.newCall(GET(url = listEntryUrl(track.media_id))) - .asObservable() - .map {response -> - var libTrack: Track? = null - response.use { - if (it.priorResponse?.isRedirect != true) { - val trackForm = Jsoup.parse(it.consumeBody()) - - libTrack = Track.create(TrackManager.MYANIMELIST).apply { - last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt() - total_chapters = trackForm.select("#totalChap").text().toInt() - status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt() - score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f - } - } + suspend fun updateLibManga(track: Track): Track { + authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))).await() + return track + } + + suspend fun findLibManga(track: Track): Track? { + return withContext(Dispatchers.IO) { + val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await() + var remoteTrack: Track? = null + response.use { + if (it.priorResponse?.isRedirect != true) { + val trackForm = Jsoup.parse(it.consumeBody()) + + remoteTrack = Track.create(TrackManager.MYANIMELIST).apply { + last_chapter_read = + trackForm.select("#add_manga_num_read_chapters").`val`().toInt() + total_chapters = trackForm.select("#totalChap").text().toInt() + status = + trackForm.select("#add_manga_status > option[selected]").`val`().toInt() + score = trackForm.select("#add_manga_score > option[selected]").`val`() + .toFloatOrNull() ?: 0f } - libTrack } + } + remoteTrack + } } - fun getLibManga(track: Track): Observable { - return findLibManga(track) - .map { it ?: throw Exception("Could not find manga") } + suspend fun getLibManga(track: Track): Track { + val result = findLibManga(track) + if (result == null) { + throw Exception("Could not find manga") + } else { + return result + } } - fun login(username: String, password: String): String { - val csrf = getSessionInfo() - - login(username, password, csrf) - - return csrf + suspend fun login(username: String, password: String): String { + return withContext(Dispatchers.IO) { + val csrf = getSessionInfo() + login(username, password, csrf) + csrf + } } - private fun getSessionInfo(): String { + private suspend fun getSessionInfo(): String { val response = client.newCall(GET(loginUrl())).execute() - return Jsoup.parse(response.consumeBody()) - .select("meta[name=csrf_token]") - .attr("content") + return Jsoup.parse(response.consumeBody()).select("meta[name=csrf_token]").attr("content") } - private fun login(username: String, password: String, csrf: String) { - val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute() + private suspend fun login(username: String, password: String, csrf: String) { + withContext(Dispatchers.IO) { + val response = + client.newCall(POST(loginUrl(), body = loginPostBody(username, password, csrf))) + .execute() - response.use { - if (response.priorResponse?.code != 302) throw Exception("Authentication error") + response.use { + if (response.priorResponse?.code != 302) throw Exception("Authentication error") + } } } - private fun getList(): Observable> { - return getListUrl() - .flatMap { url -> - getListXml(url) - } - .flatMap { doc -> - Observable.from(doc.select("manga")) - } - .map { - TrackSearch.create(TrackManager.MYANIMELIST).apply { - title = it.selectText("manga_title")!! - media_id = it.selectInt("manga_mangadb_id") - last_chapter_read = it.selectInt("my_read_chapters") - status = getStatus(it.selectText("my_status")!!) - score = it.selectInt("my_score").toFloat() - total_chapters = it.selectInt("manga_chapters") - tracking_url = mangaUrl(media_id) - } - } - .toList() - } - - private fun getListUrl(): Observable { - return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())) - .asObservable() - .map {response -> - baseUrl + Jsoup.parse(response.consumeBody()) - .select("div.goodresult") - .select("a") - .attr("href") + private suspend fun getList(): List { + val results = getListXml(getListUrl()).select("manga") + + return results.map { + TrackSearch.create(TrackManager.MYANIMELIST).apply { + title = it.selectText("manga_title")!! + media_id = it.selectInt("manga_mangadb_id") + last_chapter_read = it.selectInt("my_read_chapters") + status = getStatus(it.selectText("my_status")!!) + score = it.selectInt("my_score").toFloat() + total_chapters = it.selectInt("manga_chapters") + tracking_url = mangaUrl(media_id) } + }.toList() } - private fun getListXml(url: String): Observable { - return authClient.newCall(GET(url)) - .asObservable() - .map { response -> - Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) - } - } + private suspend fun getListUrl(): String { + return withContext(Dispatchers.IO) { + val response = + authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute() - private fun Response.consumeBody(): String? { - use { - if (it.code != 200) throw Exception("HTTP error ${it.code}") - return it.body?.string() + baseUrl + Jsoup.parse(response.consumeBody()).select("div.goodresult").select("a") + .attr("href") } } - private fun Response.consumeXmlBody(): String? { - use { res -> - if (res.code != 200) throw Exception("Export list error") - BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader -> - val sb = StringBuilder() - reader.forEachLine { line -> - sb.append(line) - } - return sb.toString() - } - } + private suspend fun getListXml(url: String): Document { + val response = authClient.newCall(GET(url)).await() + return Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser()) } companion object { @@ -205,89 +171,63 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId - private fun loginUrl() = Uri.parse(baseUrl).buildUpon() - .appendPath("login.php") - .toString() + private fun loginUrl() = Uri.parse(baseUrl).buildUpon().appendPath("login.php").toString() private fun searchUrl(query: String): String { val col = "c[]" - return Uri.parse(baseUrl).buildUpon() - .appendPath("manga.php") - .appendQueryParameter("q", query) - .appendQueryParameter(col, "a") - .appendQueryParameter(col, "b") - .appendQueryParameter(col, "c") - .appendQueryParameter(col, "d") - .appendQueryParameter(col, "e") - .appendQueryParameter(col, "g") - .toString() + return Uri.parse(baseUrl).buildUpon().appendPath("manga.php") + .appendQueryParameter("q", query).appendQueryParameter(col, "a") + .appendQueryParameter(col, "b").appendQueryParameter(col, "c") + .appendQueryParameter(col, "d").appendQueryParameter(col, "e") + .appendQueryParameter(col, "g").toString() } - private fun exportListUrl() = Uri.parse(baseUrl).buildUpon() - .appendPath("panel.php") - .appendQueryParameter("go", "export") - .toString() + private fun exportListUrl() = Uri.parse(baseUrl).buildUpon().appendPath("panel.php") + .appendQueryParameter("go", "export").toString() - private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon() - .appendPath("edit.json") - .toString() + private fun updateUrl() = + Uri.parse(baseModifyListUrl).buildUpon().appendPath("edit.json").toString() - private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() - .appendPath( "add.json") - .toString() + private fun addUrl() = + Uri.parse(baseModifyListUrl).buildUpon().appendPath("add.json").toString() - private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() - .appendPath(mediaId.toString()) - .appendPath("edit") - .toString() + private fun listEntryUrl(mediaId: Int) = + Uri.parse(baseModifyListUrl).buildUpon().appendPath(mediaId.toString()) + .appendPath("edit").toString() private fun loginPostBody(username: String, password: String, csrf: String): RequestBody { - return FormBody.Builder() - .add("user_name", username) - .add("password", password) - .add("cookie", "1") - .add("sublogin", "Login") - .add("submit", "1") - .add(CSRF, csrf) - .build() + return FormBody.Builder().add("user_name", username).add("password", password) + .add("cookie", "1").add("sublogin", "Login").add("submit", "1").add(CSRF, csrf) + .build() } private fun exportPostBody(): RequestBody { - return FormBody.Builder() - .add("type", "2") - .add("subexport", "Export My List") - .build() + return FormBody.Builder().add("type", "2").add("subexport", "Export My List").build() } private fun mangaPostPayload(track: Track): RequestBody { - val body = JSONObject() - .put("manga_id", track.media_id) - .put("status", track.status) - .put("score", track.score) - .put("num_read_chapters", track.last_chapter_read) + val body = JSONObject().put("manga_id", track.media_id).put("status", track.status) + .put("score", track.score).put("num_read_chapters", track.last_chapter_read) - return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) + return body.toString() + .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) } private fun Element.searchTitle() = select("strong").text()!! - private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt() + private fun Element.searchTotalChapters() = + if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt() - private fun Element.searchCoverUrl() = select("img") - .attr("data-src") - .split("\\?")[0] - .replace("/r/50x70/", "/") + private fun Element.searchCoverUrl() = + select("img").attr("data-src").split("\\?")[0].replace("/r/50x70/", "/") - private fun Element.searchMediaId() = select("div.picSurround") - .select("a").attr("id") - .replace("sarea", "") - .toInt() + private fun Element.searchMediaId() = + select("div.picSurround").select("a").attr("id").replace("sarea", "").toInt() - private fun Element.searchSummary() = select("div.pt4") - .first() - .ownText()!! + private fun Element.searchSummary() = select("div.pt4").first().ownText()!! - private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished" + private fun Element.searchPublishingStatus() = + if (select(TD).last().text() == "-") "Publishing" else "Finished" private fun Element.searchPublishingType() = select(TD)[2].text()!! @@ -300,6 +240,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI "Dropped" -> 4 "Plan to Read" -> 6 else -> 1 - } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 9ef078983b..bc017f8479 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,5 +1,9 @@ package eu.kanade.tachiyomi.data.track.myanimelist +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import okhttp3.Interceptor import okhttp3.Request import okhttp3.RequestBody @@ -8,20 +12,16 @@ import okhttp3.Response import okio.Buffer import org.json.JSONObject -class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor { +class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - myanimelist.ensureLoggedIn() - - val request = chain.request() - var response = chain.proceed(updateRequest(request)) + val scope = CoroutineScope(Job() + Dispatchers.Main) - if (response.code == 400) { - myanimelist.refreshLogin() - response = chain.proceed(updateRequest(request)) + override fun intercept(chain: Interceptor.Chain): Response { + scope.launch { + myanimelist.ensureLoggedIn() } - - return response + val request = chain.request() + return chain.proceed(updateRequest(request)) } private fun updateRequest(request: Request): Request { @@ -46,13 +46,15 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor private fun updateFormBody(requestBody: RequestBody): RequestBody { val formString = bodyToString(requestBody) - return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(requestBody.contentType()) + return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody( + requestBody.contentType() + ) } private fun updateJsonBody(requestBody: RequestBody): RequestBody { val jsonString = bodyToString(requestBody) val newBody = JSONObject(jsonString) - .put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) + .put(MyAnimeListApi.CSRF, myanimelist.getCSRF()) return newBody.toString().toRequestBody(requestBody.contentType()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt deleted file mode 100644 index 1f6a38b47d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/OAuth.kt +++ /dev/null @@ -1,13 +0,0 @@ -package eu.kanade.tachiyomi.data.track.shikimori - -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?) { - - // Access token lives 1 day - fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 00f7a517ff..029f8c8699 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -6,75 +6,11 @@ import com.google.gson.Gson import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import rx.Completable -import rx.Observable +import timber.log.Timber import uy.kohesive.injekt.injectLazy class Shikimori(private val context: Context, id: Int) : TrackService(id) { - override fun getScoreList(): List { - return IntRange(0, 10).map(Int::toString) - } - - override fun displayScore(track: Track): String { - return track.score.toInt().toString() - } - - override fun add(track: Track): Observable { - return api.addLibManga(track, getUsername()) - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - return api.updateLibManga(track, getUsername()) - } - - override fun bind(track: Track): Observable { - return api.findLibManga(track, getUsername()) - .flatMap { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.library_id = remoteTrack.library_id - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } - } - - override fun search(query: String): Observable> { - return api.search(query) - } - - override fun refresh(track: Track): Observable { - return api.findLibManga(track, getUsername()) - .map { remoteTrack -> - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - } - track - } - } - - companion object { - const val READING = 1 - const val COMPLETED = 2 - const val ON_HOLD = 3 - const val DROPPED = 4 - const val PLANNING = 5 - const val REPEATING = 6 - - const val DEFAULT_STATUS = READING - const val DEFAULT_SCORE = 0 - } - override val name = "Shikimori" private val gson: Gson by injectLazy() @@ -83,7 +19,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { private val api by lazy { ShikimoriApi(client, interceptor) } - override fun getLogo() = R.drawable.tracker_shikimori + override fun getLogo() = R.drawable.ic_tracker_shikimori override fun getLogoColor() = Color.rgb(40, 40, 40) @@ -91,6 +27,8 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) } + override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED + override fun getStatus(status: Int): String = with(context) { when (status) { READING -> getString(R.string.reading) @@ -98,23 +36,69 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { ON_HOLD -> getString(R.string.on_hold) DROPPED -> getString(R.string.dropped) PLANNING -> getString(R.string.plan_to_read) - REPEATING -> getString(R.string.repeating) + REPEATING -> getString(R.string.rereading) else -> "" } } - override fun login(username: String, password: String) = login(password) + override fun getScoreList(): List { + return IntRange(0, 10).map(Int::toString) + } + + override fun displayScore(track: Track): String { + return track.score.toInt().toString() + } + + override suspend fun update(track: Track): Track { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + return api.updateLibManga(track, getUsername()) + } + + override suspend fun bind(track: Track): Track { + val remoteTrack = api.findLibManga(track, getUsername()) + + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.library_id = remoteTrack.library_id + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + return api.addLibManga(track, getUsername()) + } + return track + } + + override suspend fun search(query: String) = api.search(query) + + override suspend fun refresh(track: Track): Track { + val remoteTrack = api.findLibManga(track, getUsername()) + + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + } + return track + } + + override suspend fun login(username: String, password: String) = login(password) + + suspend fun login(code: String): Boolean { + try { + val oauth = api.accessToken(code) - fun login(code: String): Completable { - return api.accessToken(code).map { oauth: OAuth? -> interceptor.newAuth(oauth) - if (oauth != null) { - val user = api.getCurrentUser() - saveCredentials(user.toString(), oauth.access_token) - } - }.doOnError { + val user = api.getCurrentUser() + saveCredentials(user.toString(), oauth.access_token) + return true + } catch (e: java.lang.Exception) { + Timber.e(e) logout() - }.toCompletable() + return false + } } fun saveToken(oauth: OAuth?) { @@ -135,4 +119,16 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) { preferences.trackToken(this).set(null) interceptor.newAuth(null) } + + companion object { + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLANNING = 5 + const val REPEATING = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 74702fcca2..42d24b51c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -13,69 +13,59 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservableSuccess -import okhttp3.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.FormBody import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody -import rx.Observable import uy.kohesive.injekt.injectLazy class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { private val gson: Gson by injectLazy() - private val parser = JsonParser() private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - fun addLibManga(track: Track, user_id: String): Observable { - val payload = jsonObject( + suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id) + + suspend fun addLibManga(track: Track, user_id: String): Track { + return withContext(Dispatchers.IO) { + val payload = jsonObject( "user_rate" to jsonObject( - "user_id" to user_id, - "target_id" to track.media_id, - "target_type" to "Manga", - "chapters" to track.last_chapter_read, - "score" to track.score.toInt(), - "status" to track.toShikimoriStatus() + "user_id" to user_id, + "target_id" to track.media_id, + "target_type" to "Manga", + "chapters" to track.last_chapter_read, + "score" to track.score.toInt(), + "status" to track.toShikimoriStatus() ) - ) - val body = payload.toString().toRequestBody(jsonime) - val request = Request.Builder() - .url("$apiUrl/v2/user_rates") - .post(body) - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { - track - } + ) + val body = payload.toString().toRequestBody(jsonime) + val request = Request.Builder().url("$apiUrl/v2/user_rates").post(body).build() + authClient.newCall(request).execute() + track + } } - fun updateLibManga(track: Track, user_id: String): Observable = addLibManga(track, user_id) - - fun search(search: String): Observable> { - val url = Uri.parse("$apiUrl/mangas").buildUpon() - .appendQueryParameter("order", "popularity") - .appendQueryParameter("search", search) - .appendQueryParameter("limit", "20") - .build() - val request = Request.Builder() - .url(url.toString()) - .get() - .build() - return authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).array - response.map { jsonToSearch(it.obj) } - } + suspend fun search(search: String): List { + return withContext(Dispatchers.IO) { + val url = + Uri.parse("$apiUrl/mangas").buildUpon().appendQueryParameter("order", "popularity") + .appendQueryParameter("search", search).appendQueryParameter("limit", "20") + .build() + val request = Request.Builder().url(url.toString()).get().build() + val netResponse = authClient.newCall(request).execute() + val responseBody = netResponse.body?.string().orEmpty() + if (responseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = JsonParser.parseString(responseBody).array + + response.map { jsonToSearch(it.obj) } + } } private fun jsonToSearch(obj: JsonObject): TrackSearch { @@ -104,56 +94,48 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter } } - fun findLibManga(track: Track, user_id: String): Observable { - val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon() + suspend fun findLibManga(track: Track, user_id: String): Track? { + return withContext(Dispatchers.IO) { + val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon() .appendQueryParameter("user_id", user_id) .appendQueryParameter("target_id", track.media_id.toString()) - .appendQueryParameter("target_type", "Manga") - .build() - val request = Request.Builder() - .url(url.toString()) - .get() - .build() - - val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon() - .appendPath(track.media_id.toString()) - .build() - val requestMangas = Request.Builder() - .url(urlMangas.toString()) - .get() - .build() - return authClient.newCall(requestMangas) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - parser.parse(responseBody).obj - }.flatMap { mangas -> - authClient.newCall(request) - .asObservableSuccess() - .map { netResponse -> - val responseBody = netResponse.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - val response = parser.parse(responseBody).array - if (response.size() > 1) { - throw Exception("Too much mangas in response") - } - val entry = response.map { - jsonToTrack(it.obj, mangas) - } - entry.firstOrNull() - } - } + .appendQueryParameter("target_type", "Manga").build() + val request = Request.Builder().url(url.toString()).get().build() + + val urlMangas = + Uri.parse("$apiUrl/mangas").buildUpon().appendPath(track.media_id.toString()) + .build() + val requestMangas = Request.Builder().url(urlMangas.toString()).get().build() + + val requestMangasResponse = authClient.newCall(requestMangas).execute() + val requestMangasBody = requestMangasResponse.body?.string().orEmpty() + val mangas = JsonParser.parseString(requestMangasBody).obj + + val requestResponse = authClient.newCall(request).execute() + val requestResponseBody = requestResponse.body?.string().orEmpty() + + if (requestResponseBody.isEmpty()) { + throw Exception("Null Response") + } + val response = JsonParser.parseString(requestResponseBody).array + if (response.size() > 1) { + throw Exception("Too much mangas in response") + } + val entry = response.map { + jsonToTrack(it.obj, mangas) + } + entry.firstOrNull() + } } fun getCurrentUser(): Int { val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string() - return parser.parse(user).obj["id"].asInt + return JsonParser.parseString(user).obj["id"].asInt } - fun accessToken(code: String): Observable { - return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse -> + suspend fun accessToken(code: String): OAuth { + return withContext(Dispatchers.IO) { + val netResponse = client.newCall(accessTokenRequest(code)).execute() val responseBody = netResponse.body?.string().orEmpty() if (responseBody.isEmpty()) { throw Exception("Null Response") @@ -162,20 +144,18 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter } } - private fun accessTokenRequest(code: String) = POST(oauthUrl, - body = FormBody.Builder() - .add("grant_type", "authorization_code") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("code", code) - .add("redirect_uri", redirectUrl) - .build() + private fun accessTokenRequest(code: String) = POST( + oauthUrl, + body = FormBody.Builder().add("grant_type", "authorization_code").add("client_id", clientId) + .add("client_secret", clientSecret).add("code", code).add("redirect_uri", redirectUrl) + .build() ) - companion object { - private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" - private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" + private const val clientId = + "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" + private const val clientSecret = + "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" private const val baseUrl = "https://shikimori.one" private const val apiUrl = "https://shikimori.one/api" @@ -189,22 +169,14 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter return "$baseMangaUrl/$remoteId" } - fun authUrl() = - Uri.parse(loginUrl).buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("redirect_uri", redirectUrl) - .appendQueryParameter("response_type", "code") - .build() - - - fun refreshTokenRequest(token: String) = POST(oauthUrl, - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .build()) + fun authUrl() = Uri.parse(loginUrl).buildUpon().appendQueryParameter("client_id", clientId) + .appendQueryParameter("redirect_uri", redirectUrl) + .appendQueryParameter("response_type", "code").build() + fun refreshTokenRequest(token: String) = POST( + oauthUrl, + body = FormBody.Builder().add("grant_type", "refresh_token").add("client_id", clientId) + .add("client_secret", clientSecret).add("refresh_token", token).build() + ) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt index 91e556bdd8..4ad7218f36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt @@ -22,3 +22,15 @@ fun toTrackStatus(status: String) = when (status) { else -> throw Exception("Unknown status") } + +data class OAuth( + val access_token: String, + val token_type: String, + val created_at: Long, + val expires_in: Long, + val refresh_token: String? +) { + + // Access token lives 1 day + fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/Release.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/Release.kt index 9ac138e980..61f2bd7870 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/Release.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/Release.kt @@ -9,5 +9,4 @@ interface Release { * @return download link of latest release. */ val downloadLink: String - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt index 4d2a1de66c..d7a99ae2ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt @@ -1,25 +1,15 @@ package eu.kanade.tachiyomi.data.updater -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker -import rx.Observable abstract class UpdateChecker { companion object { - fun getUpdateChecker(): UpdateChecker { - return if (BuildConfig.DEBUG) { - DevRepoUpdateChecker() - } else { - GithubUpdateChecker() - } - } + fun getUpdateChecker(): UpdateChecker = GithubUpdateChecker() } /** - * Returns observable containing release information + * Returns suspended result containing release information */ - abstract fun checkForUpdate(): Observable - + abstract suspend fun checkForUpdate(): UpdateResult } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateResult.kt index a59864f557..a147c01df1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateResult.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateResult.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.updater abstract class UpdateResult { - open class NewUpdate(val release: T): UpdateResult() - open class NoNewUpdate: UpdateResult() - + open class NewUpdate(val release: T) : UpdateResult() + open class NoNewUpdate : UpdateResult() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt index 5c6209ec53..b0093843ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt @@ -9,39 +9,45 @@ import com.evernote.android.job.JobManager import com.evernote.android.job.JobRequest import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.Notifications -import java.util.concurrent.TimeUnit import eu.kanade.tachiyomi.util.system.notificationManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit class UpdaterJob : Job() { override fun onRunJob(params: Params): Result { - return UpdateChecker.getUpdateChecker() - .checkForUpdate() - .map { result -> - if (result is UpdateResult.NewUpdate<*>) { - val url = result.release.downloadLink + GlobalScope.launch(Dispatchers.IO) { + val result = try { UpdateChecker.getUpdateChecker().checkForUpdate() } catch (e: Exception) { return@launch } + if (result is UpdateResult.NewUpdate<*>) { + val url = result.release.downloadLink - val intent = Intent(context, UpdaterService::class.java).apply { - putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url) - } + val intent = Intent(context, UpdaterService::class.java).apply { + putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url) + } - NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update { - setContentTitle(context.getString(R.string.app_name)) - setContentText(context.getString(R.string.update_check_notification_update_available)) - setSmallIcon(android.R.drawable.stat_sys_download_done) - color = ContextCompat.getColor(context, R.color.colorAccent) - // Download action - addAction(android.R.drawable.stat_sys_download_done, - context.getString(R.string.action_download), - PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) - } - } - Result.SUCCESS + NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update { + setContentTitle(context.getString(R.string.app_name)) + setContentText(context.getString(R.string.update_available)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + color = ContextCompat.getColor(context, R.color.colorAccent) + // Download action + addAction( + android.R.drawable.stat_sys_download_done, + context.getString(R.string.download), + PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) } - .onErrorReturn { Result.FAILURE } - // Sadly, the task needs to be synchronous. - .toBlocking() - .single() + } + Result.SUCCESS + } + return Result.SUCCESS } fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { @@ -66,5 +72,4 @@ class UpdaterJob : Job() { JobManager.instance().cancelAllForTag(TAG) } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt index 3c8d5d69b4..10a2c64406 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt @@ -41,7 +41,7 @@ internal class UpdaterNotifier(private val context: Context) { fun onDownloadStarted(title: String) { with(notification) { setContentTitle(title) - setContentText(context.getString(R.string.update_check_notification_download_in_progress)) + setContentText(context.getString(R.string.downloading)) setSmallIcon(android.R.drawable.stat_sys_download) setOngoing(true) } @@ -68,18 +68,18 @@ internal class UpdaterNotifier(private val context: Context) { */ fun onDownloadFinished(uri: Uri) { with(notification) { - setContentText(context.getString(R.string.update_check_notification_download_complete)) + setContentText(context.getString(R.string.download_complete)) setSmallIcon(android.R.drawable.stat_sys_download_done) setOnlyAlertOnce(false) setProgress(0, 0, false) // Install action setContentIntent(NotificationHandler.installApkPendingActivity(context, uri)) addAction(R.drawable.ic_system_update_grey_24dp_img, - context.getString(R.string.action_install), + context.getString(R.string.install), NotificationHandler.installApkPendingActivity(context, uri)) // Cancel action addAction(R.drawable.ic_clear_grey_24dp_img, - context.getString(R.string.action_cancel), + context.getString(R.string.cancel), NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) } notification.show() @@ -92,18 +92,18 @@ internal class UpdaterNotifier(private val context: Context) { */ fun onDownloadError(url: String) { with(notification) { - setContentText(context.getString(R.string.update_check_notification_download_error)) + setContentText(context.getString(R.string.download_error)) setSmallIcon(android.R.drawable.stat_sys_warning) setOnlyAlertOnce(false) setProgress(0, 0, false) color = ContextCompat.getColor(context, R.color.colorAccent) // Retry action addAction(R.drawable.ic_refresh_grey_24dp_img, - context.getString(R.string.action_retry), + context.getString(R.string.retry), UpdaterService.downloadApkPendingService(context, url)) // Cancel action addAction(R.drawable.ic_clear_grey_24dp_img, - context.getString(R.string.action_cancel), + context.getString(R.string.cancel), NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) } notification.show(Notifications.ID_UPDATER) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt index ddce70db74..16974b7352 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt @@ -119,5 +119,3 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) { } } } - - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoRelease.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoRelease.kt deleted file mode 100644 index ea8a79a182..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoRelease.kt +++ /dev/null @@ -1,14 +0,0 @@ -package eu.kanade.tachiyomi.data.updater.devrepo - -import eu.kanade.tachiyomi.data.updater.Release - -class DevRepoRelease(override val info: String) : Release { - - override val downloadLink: String - get() = LATEST_URL - - companion object { - const val LATEST_URL = "https://tachiyomi.kanade.eu/latest" - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateChecker.kt deleted file mode 100644 index a24036830d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateChecker.kt +++ /dev/null @@ -1,40 +0,0 @@ -package eu.kanade.tachiyomi.data.updater.devrepo - -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.data.updater.UpdateChecker -import eu.kanade.tachiyomi.data.updater.UpdateResult -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.asObservable -import okhttp3.OkHttpClient -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class DevRepoUpdateChecker : UpdateChecker() { - - private val client: OkHttpClient by lazy { - Injekt.get().client.newBuilder() - .followRedirects(false) - .build() - } - - private val versionRegex: Regex by lazy { - Regex("tachiyomi-r(\\d+).apk") - } - - override fun checkForUpdate(): Observable { - return client.newCall(GET(DevRepoRelease.LATEST_URL)).asObservable() - .map { response -> - // Get latest repo version number from header in format "Location: tachiyomi-r1512.apk" - val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1] - - if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) { - DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber")) - } else { - DevRepoUpdateResult.NoNewUpdate() - } - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateResult.kt deleted file mode 100644 index 1bda48b9c3..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/devrepo/DevRepoUpdateResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package eu.kanade.tachiyomi.data.updater.devrepo - -import eu.kanade.tachiyomi.data.updater.UpdateResult - -sealed class DevRepoUpdateResult : UpdateResult() { - - class NewUpdate(release: DevRepoRelease): UpdateResult.NewUpdate(release) - class NoNewUpdate: UpdateResult.NoNewUpdate() - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubRelease.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubRelease.kt index 4e4d7feca2..09f1b37d01 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubRelease.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubRelease.kt @@ -11,9 +11,11 @@ import eu.kanade.tachiyomi.data.updater.Release * @param info log of latest release. * @param assets assets of latest release. */ -class GithubRelease(@SerializedName("tag_name") val version: String, - @SerializedName("body") override val info: String, - @SerializedName("assets") private val assets: List): Release { +class GithubRelease( + @SerializedName("tag_name") val version: String, + @SerializedName("body") override val info: String, + @SerializedName("assets") private val assets: List +) : Release { /** * Get download link of latest release from the assets. @@ -28,4 +30,3 @@ class GithubRelease(@SerializedName("tag_name") val version: String, */ inner class Assets(@SerializedName("browser_download_url") val downloadLink: String) } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt index 052684bd03..8f9a4857bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt @@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.data.updater.github import eu.kanade.tachiyomi.network.NetworkHelper import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET -import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -19,7 +17,6 @@ interface GithubService { val restAdapter = Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .client(Injekt.get().client) .build() @@ -28,6 +25,5 @@ interface GithubService { } @GET("/repos/Jays2Kings/tachiyomiJ2K/releases/latest") - fun getLatestVersion(): Observable - + suspend fun getLatestVersion(): GithubRelease } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt index 6fc6297409..f90d8c8179 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt @@ -3,23 +3,20 @@ package eu.kanade.tachiyomi.data.updater.github import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.updater.UpdateChecker import eu.kanade.tachiyomi.data.updater.UpdateResult -import rx.Observable -class GithubUpdateChecker : UpdateChecker() { + class GithubUpdateChecker : UpdateChecker() { private val service: GithubService = GithubService.create() - override fun checkForUpdate(): Observable { - return service.getLatestVersion().map { release -> - val newVersion = release.version.replace("[^\\d.]".toRegex(), "") + override suspend fun checkForUpdate(): UpdateResult { + val release = service.getLatestVersion() + val newVersion = release.version.replace("[^\\d.]".toRegex(), "") - // Check if latest version is different from current version - if (newVersion != BuildConfig.VERSION_NAME) { - GithubUpdateResult.NewUpdate(release) - } else { - GithubUpdateResult.NoNewUpdate() - } + // Check if latest version is different from current version + return if (newVersion != BuildConfig.VERSION_NAME) { + GithubUpdateResult.NewUpdate(release) + } else { + GithubUpdateResult.NoNewUpdate() } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateResult.kt index fcb304604b..8462f937ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateResult.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateResult.kt @@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult sealed class GithubUpdateResult : UpdateResult() { - class NewUpdate(release: GithubRelease): UpdateResult.NewUpdate(release) + class NewUpdate(release: GithubRelease) : UpdateResult.NewUpdate(release) class NoNewUpdate : UpdateResult.NoNewUpdate() - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index ea12b35d52..7a50c38730 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.extension import android.content.Context +import android.graphics.drawable.Drawable import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -11,9 +12,12 @@ import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionLoader +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.util.system.launchNow +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.withContext import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -29,8 +33,8 @@ import uy.kohesive.injekt.api.get * @param preferences The application preferences. */ class ExtensionManager( - private val context: Context, - private val preferences: PreferencesHelper = Injekt.get() + private val context: Context, + private val preferences: PreferencesHelper = Injekt.get() ) { /** @@ -55,8 +59,31 @@ class ExtensionManager( private set(value) { field = value installedExtensionsRelay.call(value) + listener?.extensionsUpdated() } + private var listener: ExtensionsChangedListener? = null + + fun setListener(listener: ExtensionsChangedListener) { + this.listener = listener + } + + fun removeListener(listener: ExtensionsChangedListener) { + if (this.listener == listener) + this.listener = null + } + + fun getAppIconForSource(source: Source): Drawable? { + val pkgName = + installedExtensions.find { ext -> ext.sources.any { it.id == source.id } }?.pkgName + return if (pkgName != null) try { + context.packageManager.getApplicationIcon(pkgName) + } catch (e: Exception) { + null + } + else null + } + /** * Relay used to notify the available extensions. */ @@ -70,6 +97,7 @@ class ExtensionManager( field = value availableExtensionsRelay.call(value) updatedInstalledExtensionsStatuses(value) + listener?.extensionsUpdated() } /** @@ -84,6 +112,7 @@ class ExtensionManager( private set(value) { field = value untrustedExtensionsRelay.call(value) + listener?.extensionsUpdated() } /** @@ -153,14 +182,26 @@ class ExtensionManager( } } + /** + * Finds the available extensions in the [api] and updates [availableExtensions]. + */ + suspend fun findAvailableExtensionsAsync() { + withContext(Dispatchers.IO) { + availableExtensions = try { + api.findExtensions() + } catch (e: Exception) { + emptyList() + } + } + } + /** * Sets the update field of the installed extensions with the given [availableExtensions]. * * @param availableExtensions The list of extensions given by the [api]. */ private fun updatedInstalledExtensionsStatuses(availableExtensions: List) { - if (availableExtensions.isEmpty()) - { + if (availableExtensions.isEmpty()) { preferences.extensionUpdatesCount().set(0) return } @@ -208,7 +249,7 @@ class ExtensionManager( * * @param extension The extension to be updated. */ - fun updateExtension(extension: Extension.Installed): Observable { + fun updateExtension(extension: Extension.Installed): Observable { val availableExt = availableExtensions.find { it.pkgName == extension.pkgName } ?: return Observable.empty() return installExtension(availableExt) @@ -343,5 +384,8 @@ class ExtensionManager( } return this } +} +interface ExtensionsChangedListener { + fun extensionsUpdated() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt index 02c8d1afdc..53202e4614 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.extension - import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -23,7 +22,7 @@ class ExtensionUpdateJob : Job() { override fun onRunJob(params: Params): Result { GlobalScope.launch(Dispatchers.IO) { - val pendingUpdates = ExtensionGithubApi().checkforUpdates(context) + val pendingUpdates = ExtensionGithubApi().checkForUpdates(context) if (pendingUpdates.isNotEmpty()) { val names = pendingUpdates.map { it.name } val preferences: PreferencesHelper by injectLazy() @@ -33,17 +32,11 @@ class ExtensionUpdateJob : Job() { context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) { setContentTitle( context.resources.getQuantityString( - R.plurals.update_check_notification_ext_updates, names + R.plurals.extension_updates_available, names .size, names.size ) ) - val extNames = if (names.size > 5) { - "${names.take(4).joinToString(", ")}, " + - context.resources.getQuantityString( - R.plurals.notification_and_n_more_ext, - (names.size - 4), (names.size - 4) - ) - } else names.joinToString(", ") + val extNames = names.joinToString(", ") setContentText(extNames) setStyle(NotificationCompat.BigTextStyle().bigText(extNames)) setSmallIcon(R.drawable.ic_extension_update) @@ -77,4 +70,4 @@ class ExtensionUpdateJob : Job() { JobManager.instance().cancelAllForTag(TAG) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index d55c4b9357..79fff8b97e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -7,18 +7,16 @@ import com.github.salomonbrys.kotson.int import com.github.salomonbrys.kotson.string import com.google.gson.Gson import com.google.gson.JsonArray -import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.await import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import eu.kanade.tachiyomi.network.await import okhttp3.Response import uy.kohesive.injekt.injectLazy -import java.lang.Exception internal class ExtensionGithubApi { @@ -30,11 +28,11 @@ internal class ExtensionGithubApi { val call = GET("$REPO_URL/index.json") return withContext(Dispatchers.IO) { - parseResponse(network.client.newCall(call).await()) + parseResponse(network.client.newCall(call).await()) } } - suspend fun checkforUpdates(context: Context): List { + suspend fun checkForUpdates(context: Context): List { return withContext(Dispatchers.IO) { val call = GET("$REPO_URL/index.json") val response = network.client.newCall(call).await() @@ -43,9 +41,9 @@ internal class ExtensionGithubApi { val extensions = parseResponse(response) val extensionsWithUpdate = mutableListOf() - val installedExtensions = ExtensionLoader.loadExtensions(context) - .filterIsInstance() - .map { it.extension } + val installedExtensions = + ExtensionLoader.loadExtensions(context).filterIsInstance() + .map { it.extension } val mutInstalledExtensions = installedExtensions.toMutableList() for (installedExt in mutInstalledExtensions) { val pkgName = installedExt.pkgName @@ -86,6 +84,7 @@ internal class ExtensionGithubApi { } companion object { - private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo" + private const val REPO_URL = + "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index f0a53690f5..7fb384cc01 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -10,28 +10,33 @@ sealed class Extension { abstract val versionCode: Int abstract val lang: String? - data class Installed(override val name: String, - override val pkgName: String, - override val versionName: String, - override val versionCode: Int, - val sources: List, - override val lang: String, - val hasUpdate: Boolean = false, - val isObsolete: Boolean = false) : Extension() + data class Installed( + override val name: String, + override val pkgName: String, + override val versionName: String, + override val versionCode: Int, + val sources: List, + override val lang: String, + val hasUpdate: Boolean = false, + val isObsolete: Boolean = false + ) : Extension() - data class Available(override val name: String, - override val pkgName: String, - override val versionName: String, - override val versionCode: Int, - override val lang: String, - val apkName: String, - val iconUrl: String) : Extension() - - data class Untrusted(override val name: String, - override val pkgName: String, - override val versionName: String, - override val versionCode: Int, - val signatureHash: String, - override val lang: String? = null) : Extension() + data class Available( + override val name: String, + override val pkgName: String, + override val versionName: String, + override val versionCode: Int, + override val lang: String, + val apkName: String, + val iconUrl: String + ) : Extension() + data class Untrusted( + override val name: String, + override val pkgName: String, + override val versionName: String, + override val versionCode: Int, + val signatureHash: String, + override val lang: String? = null + ) : Extension() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index bcabce0f42..ed84370de7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -93,8 +93,8 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : * @param intent The intent containing the package name of the extension. */ private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { - val pkgName = getPackageNameFromIntent(intent) ?: - return LoadResult.Error("Package name not found") + val pkgName = getPackageNameFromIntent(intent) + ?: return LoadResult.Error("Package name not found") return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await() } @@ -114,5 +114,4 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : fun onExtensionUntrusted(extension: Extension.Untrusted) fun onPackageUninstalled(pkgName: String) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 8c444a98ee..2051d717fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -245,5 +245,4 @@ internal class ExtensionInstaller(private val context: Context) { const val APK_MIME = "application/vnd.android.package-archive" const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 35a5948f22..29525d8159 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -179,5 +179,4 @@ internal object ExtensionLoader { null } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index 7960bdcb6e..5f57390e9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -2,17 +2,20 @@ package eu.kanade.tachiyomi.network import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Handler import android.os.Looper import android.webkit.WebSettings import android.webkit.WebView +import android.widget.Toast +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.WebViewClientCompat +import eu.kanade.tachiyomi.util.system.isOutdated +import eu.kanade.tachiyomi.util.system.toast import okhttp3.Cookie +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import okhttp3.HttpUrl.Companion.toHttpUrl import uy.kohesive.injekt.injectLazy import java.io.IOException import java.util.concurrent.CountDownLatch @@ -20,8 +23,6 @@ import java.util.concurrent.TimeUnit class CloudflareInterceptor(private val context: Context) : Interceptor { - private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") - private val handler = Handler(Looper.getMainLooper()) private val networkHelper: NetworkHelper by injectLazy() @@ -43,73 +44,87 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { val response = chain.proceed(originalRequest) // Check if Cloudflare anti-bot is on - if (response.code == 503 && response.header("Server") in serverCheck) { - try { - response.close() - networkHelper.cookieManager.remove(originalRequest.url, listOf("__cfduid", "cf_clearance"), 0) - val oldCookie = networkHelper.cookieManager.get(originalRequest.url) - .firstOrNull { it.name == "cf_clearance" } - return if (resolveWithWebView(originalRequest, oldCookie)) { - chain.proceed(originalRequest) - } else { - throw IOException("Failed to bypass Cloudflare!") - } - } catch (e: Exception) { - // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that - // we don't crash the entire app - throw IOException(e) - } + if (response.code != 503 || response.header("Server") !in SERVER_CHECK) { + return response } - return response + try { + response.close() + networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0) + val oldCookie = networkHelper.cookieManager.get(originalRequest.url) + .firstOrNull { it.name == "cf_clearance" } + resolveWithWebView(originalRequest, oldCookie) + + // Avoid use empty User-Agent + return if (originalRequest.header("User-Agent").isNullOrEmpty()) { + val newRequest = originalRequest + .newBuilder() + .removeHeader("User-Agent") + .addHeader("User-Agent", + DEFAULT_USERAGENT) + .build() + chain.proceed(newRequest) + } else { + chain.proceed(originalRequest) + } + } catch (e: Exception) { + // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that + // we don't crash the entire app + throw IOException(e) + } } @SuppressLint("SetJavaScriptEnabled") - private fun resolveWithWebView(request: Request, oldCookie: Cookie?): Boolean { + private fun resolveWithWebView(request: Request, oldCookie: Cookie?) { // We need to lock this thread until the WebView finds the challenge solution url, because // OkHttp doesn't support asynchronous interceptors. val latch = CountDownLatch(1) var webView: WebView? = null + var challengeFound = false var cloudflareBypassed = false + var isWebviewOutdated = false val origRequestUrl = request.url.toString() val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } + val withUserAgent = request.header("User-Agent").isNullOrEmpty() handler.post { - val view = WebView(context.applicationContext) - webView = view - view.settings.javaScriptEnabled = true - view.settings.userAgentString = request.header("User-Agent") - view.webViewClient = object : WebViewClientCompat() { + val webview = WebView(context) + webView = webview + webview.settings.javaScriptEnabled = true + + // Avoid set empty User-Agent, Chromium WebView will reset to default if empty + webview.settings.userAgentString = request.header("User-Agent") + ?: DEFAULT_USERAGENT + webview.webViewClient = object : WebViewClientCompat() { override fun onPageFinished(view: WebView, url: String) { fun isCloudFlareBypassed(): Boolean { return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) - .firstOrNull { it.name == "cf_clearance" } - .let { it != null && it != oldCookie } + .firstOrNull { it.name == "cf_clearance" } + .let { it != null && (it != oldCookie || withUserAgent) } } if (isCloudFlareBypassed()) { cloudflareBypassed = true latch.countDown() } - // Http error codes are only received since M - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - url == origRequestUrl && !challengeFound - ) { + + // HTTP error codes are only received since M + if (url == origRequestUrl && !challengeFound) { // The first request didn't return the challenge, abort. latch.countDown() } } override fun onReceivedErrorCompat( - view: WebView, - errorCode: Int, - description: String?, - failingUrl: String, - isMainFrame: Boolean + view: WebView, + errorCode: Int, + description: String?, + failingUrl: String, + isMainFrame: Boolean ) { if (isMainFrame) { if (errorCode == 503) { @@ -122,6 +137,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { } } } + webView?.loadUrl(origRequestUrl, headers) } @@ -130,10 +146,28 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { latch.await(12, TimeUnit.SECONDS) handler.post { + if (!cloudflareBypassed) { + isWebviewOutdated = webView?.isOutdated() == true + } + webView?.stopLoading() webView?.destroy() } - return cloudflareBypassed + + // Throw exception if we failed to bypass Cloudflare + if (!cloudflareBypassed) { + // Prompt user to update WebView if it seems too outdated + if (isWebviewOutdated) { + context.toast(R.string.please_update_webview, Toast.LENGTH_LONG) + } + + throw Exception(context.getString(R.string.failed_to_bypass_cloudflare)) + } } + companion object { + private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") + private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance") + private const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)" + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 21445593e4..b789774461 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -21,5 +21,4 @@ class NetworkHelper(context: Context) { val cloudflareClient = client.newBuilder() .addInterceptor(CloudflareInterceptor(context)) .build() - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index dc76ff0e7a..a652e2ba1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -1,12 +1,20 @@ package eu.kanade.tachiyomi.network import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.* +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response import rx.Observable import rx.Producer import rx.Subscription +import java.io.BufferedReader import java.io.IOException +import java.io.InputStreamReader import java.util.concurrent.atomic.AtomicBoolean +import java.util.zip.GZIPInputStream import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -94,3 +102,25 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene return progressClient.newCall(request) } + +fun MediaType.Companion.jsonType(): MediaType = "application/json; charset=utf-8".toMediaTypeOrNull()!! + +fun Response.consumeBody(): String? { + use { + if (it.code != 200) throw Exception("HTTP error ${it.code}") + return it.body?.string() + } +} + +fun Response.consumeXmlBody(): String? { + use { res -> + if (res.code != 200) throw Exception("Export list error") + BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader -> + val sb = StringBuilder() + reader.forEachLine { line -> + sb.append(line) + } + return sb.toString() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt index 4bebcf87dd..2e219895fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt @@ -2,4 +2,4 @@ package eu.kanade.tachiyomi.network interface ProgressListener { fun update(bytesRead: Long, contentLength: Long, done: Boolean) -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index 8308acc1c2..ff56520b55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -2,7 +2,11 @@ package eu.kanade.tachiyomi.network import okhttp3.MediaType import okhttp3.ResponseBody -import okio.* +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer import java.io.IOException class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt index 9b2697a514..5ed56dcb2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt @@ -1,15 +1,21 @@ package eu.kanade.tachiyomi.network -import okhttp3.* +import okhttp3.CacheControl +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.Request +import okhttp3.RequestBody import java.util.concurrent.TimeUnit.MINUTES private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() private val DEFAULT_HEADERS = Headers.Builder().build() private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() -fun GET(url: String, - headers: Headers = DEFAULT_HEADERS, - cache: CacheControl = DEFAULT_CACHE_CONTROL): Request { +fun GET( + url: String, + headers: Headers = DEFAULT_HEADERS, + cache: CacheControl = DEFAULT_CACHE_CONTROL +): Request { return Request.Builder() .url(url) @@ -18,10 +24,12 @@ fun GET(url: String, .build() } -fun POST(url: String, - headers: Headers = DEFAULT_HEADERS, - body: RequestBody = DEFAULT_BODY, - cache: CacheControl = DEFAULT_CACHE_CONTROL): Request { +fun POST( + url: String, + headers: Headers = DEFAULT_HEADERS, + body: RequestBody = DEFAULT_BODY, + cache: CacheControl = DEFAULT_CACHE_CONTROL +): Request { return Request.Builder() .url(url) diff --git a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt index b782193db1..d133496a5a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.smartsearch - import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.CatalogueSource @@ -16,8 +15,10 @@ import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy import kotlin.coroutines.CoroutineContext -class SmartSearchEngine(parentContext: CoroutineContext, - val extraSearchParams: String? = null): CoroutineScope { +class SmartSearchEngine( + parentContext: CoroutineContext, + val extraSearchParams: String? = null +) : CoroutineScope { override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default private val db: DatabaseHelper by injectLazy() @@ -55,7 +56,7 @@ class SmartSearchEngine(parentContext: CoroutineContext, suspend fun normalSearch(source: CatalogueSource, title: String): SManga? { val eligibleManga = supervisorScope { - val searchQuery = if(extraSearchParams != null) { + val searchQuery = if (extraSearchParams != null) { "$title ${extraSearchParams.trim()}" } else title val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io()) @@ -64,7 +65,7 @@ class SmartSearchEngine(parentContext: CoroutineContext, return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0)) searchResults.mangas.map { - val normalizedDistance = normalizedLevenshtein.similarity(title, it.originalTitle()) + val normalizedDistance = normalizedLevenshtein.similarity(title, it.title) SearchEntry(it, normalizedDistance) }.filter { (_, normalizedDistance) -> normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD @@ -88,7 +89,7 @@ class SmartSearchEngine(parentContext: CoroutineContext, }.toMap() // Reverse pairs if reading backwards - if(!readForward) { + if (!readForward) { val tmp = openingBracketPairs openingBracketPairs = closingBracketPairs closingBracketPairs = tmp @@ -97,16 +98,16 @@ class SmartSearchEngine(parentContext: CoroutineContext, val depthPairs = bracketPairs.map { 0 }.toMutableList() val result = StringBuilder() - for(c in if(readForward) text else text.reversed()) { + for (c in if (readForward) text else text.reversed()) { val openingBracketDepthIndex = openingBracketPairs[c] - if(openingBracketDepthIndex != null) { + if (openingBracketDepthIndex != null) { depthPairs[openingBracketDepthIndex]++ } else { val closingBracketDepthIndex = closingBracketPairs[c] - if(closingBracketDepthIndex != null) { + if (closingBracketDepthIndex != null) { depthPairs[closingBracketDepthIndex]-- } else { - if(depthPairs.all { it <= 0 }) { + if (depthPairs.all { it <= 0 }) { result.append(c) } else { // In brackets, do not append to result @@ -146,4 +147,4 @@ class SmartSearchEngine(parentContext: CoroutineContext, } } -data class SearchEntry(val manga: SManga, val dist: Double) \ No newline at end of file +data class SearchEntry(val manga: SManga, val dist: Double) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt index f5f11a00bc..c78033ea60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -43,4 +43,4 @@ interface CatalogueSource : Source { * Returns the list of filters for the source. */ fun getFilterList(): FilterList -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 7dd9639d63..25cfb9c475 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -33,7 +33,8 @@ class LocalSource(private val context: Context) : CatalogueSource { companion object { private val COVER_NAME = "cover.jpg" private val POPULAR_FILTERS = FilterList(OrderBy()) - private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) }) + private val LATEST_FILTERS = + FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) }) private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) val ID = 0L @@ -62,36 +63,41 @@ class LocalSource(private val context: Context) : CatalogueSource { } override val id = ID - override val name = context.getString(R.string.local_source) + override val name = context.getString(R.string.local_library) override val lang = "" override val supportsLatest = true - override fun toString() = context.getString(R.string.local_source) + override fun toString() = context.getString(R.string.local_library) override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList + ): Observable { val baseDirs = getBaseDirectories(context) - val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L - var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() } - .flatten() - .filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time } - .distinctBy { it.name } + val time = + if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L + var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }.flatten().filter { + it.isDirectory && 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 when (state?.index) { 0 -> { - if (state.ascending) - mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } - else - mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } + if (state.ascending) mangaDirs = + mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } + else mangaDirs = + mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } } 1 -> { - if (state.ascending) - mangaDirs = mangaDirs.sortedBy(File::lastModified) - else - mangaDirs = mangaDirs.sortedByDescending(File::lastModified) + if (state.ascending) mangaDirs = mangaDirs.sortedBy(File::lastModified) + else mangaDirs = mangaDirs.sortedByDescending(File::lastModified) } } @@ -129,44 +135,42 @@ class LocalSource(private val context: Context) : CatalogueSource { override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) override fun fetchMangaDetails(manga: SManga): Observable { - getBaseDirectories(context) - .mapNotNull { File(it, manga.url).listFiles()?.toList() } - .flatten() - .filter { it.extension.equals("json") } - .firstOrNull() - ?.apply { - val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java) - manga.title = json["title"]?.asString ?: manga.title - manga.author = json["author"]?.asString ?: manga.author - manga.artist = json["artist"]?.asString ?: manga.artist - manga.description = json["description"]?.asString ?: manga.description - manga.genre = json["genre"]?.asJsonArray - ?.map { it.asString } - ?.joinToString(", ") + getBaseDirectories(context).mapNotNull { File(it, manga.url).listFiles()?.toList() } + .flatten().filter { it.extension.equals("json") }.firstOrNull()?.apply { + val json = Gson().fromJson( + Scanner(this).useDelimiter("\\Z").next(), + JsonObject::class.java + ) + manga.title = json["title"]?.asString ?: manga.title + manga.author = json["author"]?.asString ?: manga.author + manga.artist = json["artist"]?.asString ?: manga.artist + manga.description = json["description"]?.asString ?: manga.description + manga.genre = json["genre"]?.asJsonArray?.map { it.asString }?.joinToString(", ") ?: manga.genre - manga.status = json["status"]?.asInt ?: manga.status - } + manga.status = json["status"]?.asInt ?: manga.status + } return Observable.just(manga) } fun updateMangaInfo(manga: SManga) { - val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find { it - .exists() } ?: return + val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find { + it.exists() + } ?: return val gson = GsonBuilder().setPrettyPrinting().create() val file = File(directory, "info.json") file.writeText(gson.toJson(manga.toJson())) } - fun SManga.toJson():MangaJson { + 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? + val title: String, + val author: String?, + val artist: String?, + val description: String?, + val genre: Array? ) { override fun equals(other: Any?): Boolean { @@ -186,10 +190,9 @@ class LocalSource(private val context: Context) : CatalogueSource { } override fun fetchChapterList(manga: SManga): Observable> { - val chapters = getBaseDirectories(context) - .mapNotNull { File(it, manga.url).listFiles()?.toList() } - .flatten() - .filter { it.isDirectory || isSupportedFile(it.extension) } + val chapters = + getBaseDirectories(context).mapNotNull { File(it, manga.url).listFiles()?.toList() } + .flatten().filter { it.isDirectory || isSupportedFile(it.extension) } .map { chapterFile -> SChapter.create().apply { url = "${manga.url}/${chapterFile.name}" @@ -198,13 +201,13 @@ class LocalSource(private val context: Context) : CatalogueSource { } else { chapterFile.nameWithoutExtension } - val chapNameCut = chapName.replace(manga.originalTitle(), "", true).trim(' ', '-', '_') + val chapNameCut = + chapName.replace(manga.title, "", true).trim(' ', '-', '_') name = if (chapNameCut.isEmpty()) chapName else chapNameCut date_upload = chapterFile.lastModified() ChapterRecognition.parseChapterNumber(this, manga) } - } - .sortedWith(Comparator { c1, c2 -> + }.sortedWith(Comparator { c1, c2 -> val c = c2.chapter_number.compareTo(c1.chapter_number) if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c }) @@ -251,35 +254,42 @@ class LocalSource(private val context: Context) : CatalogueSource { val format = getFormat(chapter) return when (format) { is Format.Directory -> { - val entry = format.file.listFiles() - .sortedWith(Comparator { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) + val entry = format.file.listFiles().sortedWith(Comparator { f1, f2 -> + f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) + }) .find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } - entry?.let { updateCover(context, manga, it.inputStream())} + entry?.let { updateCover(context, manga, it.inputStream()) } } is Format.Zip -> { ZipFile(format.file).use { zip -> - val entry = zip.entries().toList() - .sortedWith(Comparator { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) - .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } + val entry = zip.entries().toList().sortedWith(Comparator { f1, f2 -> + f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) + }).find { + !it.isDirectory && ImageUtil.isImage(it.name) { + zip.getInputStream(it) + } + } - entry?.let { updateCover(context, manga, zip.getInputStream(it) )} + entry?.let { updateCover(context, manga, zip.getInputStream(it)) } } } is Format.Rar -> { Archive(format.file).use { archive -> - val entry = archive.fileHeaders - .sortedWith(Comparator { f1, f2 -> f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString) }) - .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } + val entry = archive.fileHeaders.sortedWith(Comparator { f1, f2 -> + f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString) + }).find { + !it.isDirectory && ImageUtil.isImage(it.fileNameString) { + archive.getInputStream(it) + } + } - entry?.let { updateCover(context, manga, archive.getInputStream(it) )} + entry?.let { updateCover(context, manga, archive.getInputStream(it)) } } } is Format.Epub -> { EpubFile(format.file).use { epub -> - val entry = epub.getImagesFromPages() - .firstOrNull() - ?.let { epub.getEntry(it) } + val entry = epub.getImagesFromPages().firstOrNull()?.let { epub.getEntry(it) } entry?.let { updateCover(context, manga, epub.getInputStream(it)) } } @@ -287,15 +297,15 @@ class LocalSource(private val context: Context) : CatalogueSource { } } - private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Filter.Sort.Selection(0, true)) + private class OrderBy : + Filter.Sort("Order by", arrayOf("Title", "Date"), Filter.Sort.Selection(0, true)) override fun getFilterList() = FilterList(OrderBy()) sealed class Format { data class Directory(val file: File) : Format() data class Zip(val file: File) : Format() - data class Rar(val file: File): Format() + data class Rar(val file: File) : Format() data class Epub(val file: File) : Format() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt index 7a5f43a846..9fb88c7337 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt @@ -1,9 +1,15 @@ package eu.kanade.tachiyomi.source +import android.graphics.drawable.Drawable +import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** * A basic interface for creating a source. It could be an online source, a local source, etc... @@ -40,5 +46,19 @@ interface Source { * @param chapter the chapter. */ fun fetchPageList(chapter: SChapter): Observable> +} -} \ No newline at end of file +suspend fun Source.fetchMangaDetailsAsync(manga: SManga): SManga? { + return withContext(Dispatchers.IO) { + fetchMangaDetails(manga).toBlocking().single() + } +} + +suspend fun Source.fetchChapterListAsync(manga: SManga): List? { + return withContext(Dispatchers.IO) { + fetchChapterList(manga).toBlocking().single() + } +} + +fun Source.icon(): Drawable? = + Injekt.get().getAppIconForSource(this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index c63951811c..83c719428b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -68,7 +68,7 @@ open class SourceManager(private val context: Context) { } private fun getSourceNotInstalledException(): Exception { - return SourceNotFoundException(context.getString(R.string.source_not_installed, id + return SourceNotFoundException(context.getString(R.string.source_not_installed_, id .toString()), id) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt index 8cd520d22d..f82ddfc21c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt @@ -17,10 +17,10 @@ sealed class Filter(val name: String, var state: T) { const val STATE_EXCLUDE = 2 } } - abstract class Group(name: String, state: List): Filter>(name, state) + abstract class Group(name: String, state: List) : Filter>(name, state) - abstract class Sort(name: String, val values: Array, state: Selection? = null) - : Filter(name, state) { + abstract class Sort(name: String, val values: Array, state: Selection? = null) : + Filter(name, state) { data class Selection(val index: Int, val ascending: Boolean) } @@ -36,5 +36,4 @@ sealed class Filter(val name: String, var state: T) { result = 31 * result + (state?.hashCode() ?: 0) return result } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt index e24db65b65..42b6bc74ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt @@ -3,5 +3,4 @@ package eu.kanade.tachiyomi.source.model data class FilterList(val list: List>) : List> by list { constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt index 12dd172a74..a377c36eaa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt @@ -1,3 +1,3 @@ package eu.kanade.tachiyomi.source.model -data class MangasPage(val mangas: List, val hasNextPage: Boolean) \ No newline at end of file +data class MangasPage(val mangas: List, val hasNextPage: Boolean) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt index a0c0b1989e..1ca0778b6f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -5,10 +5,10 @@ import eu.kanade.tachiyomi.network.ProgressListener import rx.subjects.Subject open class Page( - val index: Int, - val url: String = "", - var imageUrl: String? = null, - @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions + val index: Int, + val url: String = "", + var imageUrl: String? = null, + @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions ) : ProgressListener { val number: Int @@ -18,12 +18,19 @@ open class Page( set(value) { field = value statusSubject?.onNext(value) + statusCallback?.invoke(this) } @Transient @Volatile var progress: Int = 0 + set(value) { + field = value + statusCallback?.invoke(this) + } @Transient private var statusSubject: Subject? = null + @Transient private var statusCallback: ((Page) -> Unit)? = null + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { progress = if (contentLength > 0) { (100 * bytesRead / contentLength).toInt() @@ -36,6 +43,10 @@ open class Page( this.statusSubject = subject } + fun setStatusCallback(f: ((Page) -> Unit)?) { + statusCallback = f + } + companion object { const val QUEUE = 0 const val LOAD_PAGE = 1 @@ -43,5 +54,4 @@ open class Page( const val READY = 3 const val ERROR = 4 } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt index 0017e51d63..f53bbe8f0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt @@ -27,5 +27,4 @@ interface SChapter : Serializable { return SChapterImpl() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt index 4fa55141f4..4d5e43f1e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt @@ -10,6 +10,5 @@ class SChapterImpl : SChapter { override var chapter_number: Float = -1f - override var scanlator: String? = null - -} \ No newline at end of file + override var scanlator: String? = null +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index 7ed9f339ad..123affee08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -23,68 +23,22 @@ interface SManga : Serializable { var initialized: Boolean - fun currentTitle(): String { - val splitTitle = title.split(splitter) - return splitTitle.first() - } - - fun originalTitle(): String { - val splitTitle = title.split(splitter) - return splitTitle.last() - } - - fun currentGenres() = split(genre, true) - - fun originalGenres() = split(genre, false) - - fun currentDesc() = split(description, true) - - fun originalDesc() = split(description, false) - - fun currentAuthor() = split(author, true) - - fun originalAuthor() = split(author, false) - - fun currentArtist() = split(artist, true) - - fun originalArtist() = split(artist, false) - - private fun split(string: String?, first: Boolean):String? { - val split = string?.split(splitter) ?: return null - val s = if (first) split.first() else split.last() - return if (s.isBlank()) null else s - } + fun hasCustomCover() = thumbnail_url?.startsWith("Custom-") == true fun copyFrom(other: SManga) { if (other.author != null) - author = if (currentAuthor() != originalAuthor()) { - val current = currentAuthor() - val og = other.author - "${current}$splitter${og}" - } else other.author + author = other.author if (other.artist != null) - artist = if (currentArtist() != originalArtist()) { - val current = currentArtist() - val og = other.artist - "${current}$splitter${og}" - } else other.artist + artist = other.artist if (other.description != null) - description = if (currentDesc() != originalDesc()) { - val current = currentDesc() - val og = other.description - "${current}$splitter${og}" - } else other.description + description = other.description if (other.genre != null) - genre = if (currentGenres() != originalGenres()) { - val current = currentGenres() - val og = other.genre - "${current}$splitter${og}" - } else other.genre + genre = other.genre - if (other.thumbnail_url != null) + if (other.thumbnail_url != null && !hasCustomCover()) thumbnail_url = other.thumbnail_url status = other.status @@ -98,11 +52,9 @@ interface SManga : Serializable { const val ONGOING = 1 const val COMPLETED = 2 const val LICENSED = 3 - const val splitter = "▒ ▒∩▒" fun create(): SManga { return MangaImpl() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 86b020be36..1a0e5b49f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -5,14 +5,17 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.* +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 okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable import uy.kohesive.injekt.injectLazy -import java.lang.Exception import java.net.URI import java.net.URISyntaxException import java.security.MessageDigest @@ -70,7 +73,7 @@ abstract class HttpSource : CatalogueSource { /** * Headers builder for requests. Implementations can override this method for custom headers. */ - open protected fun headersBuilder() = Headers.Builder().apply { + protected open fun headersBuilder() = Headers.Builder().apply { add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") } @@ -98,14 +101,14 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ - abstract protected fun popularMangaRequest(page: Int): Request + protected abstract fun popularMangaRequest(page: Int): Request /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - abstract protected fun popularMangaParse(response: Response): MangasPage + protected abstract fun popularMangaParse(response: Response): MangasPage /** * Returns an observable containing a page with a list of manga. Normally it's not needed to @@ -130,14 +133,14 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request + protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - abstract protected fun searchMangaParse(response: Response): MangasPage + protected abstract fun searchMangaParse(response: Response): MangasPage /** * Returns an observable containing a page with a list of latest manga updates. @@ -157,14 +160,14 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ - abstract protected fun latestUpdatesRequest(page: Int): Request + protected abstract fun latestUpdatesRequest(page: Int): Request /** * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. */ - abstract protected fun latestUpdatesParse(response: Response): MangasPage + protected abstract fun latestUpdatesParse(response: Response): MangasPage /** * Returns an observable with the updated details for a manga. Normally it's not needed to @@ -195,7 +198,7 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - abstract protected fun mangaDetailsParse(response: Response): SManga + protected abstract fun mangaDetailsParse(response: Response): SManga /** * Returns an observable with the updated chapter list for a manga. Normally it's not needed to @@ -221,7 +224,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to look for chapters. */ - open protected fun chapterListRequest(manga: SManga): Request { + protected open fun chapterListRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } @@ -230,7 +233,7 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - abstract protected fun chapterListParse(response: Response): List + protected abstract fun chapterListParse(response: Response): List /** * Returns an observable with the page list for a chapter. @@ -251,7 +254,7 @@ abstract class HttpSource : CatalogueSource { * * @param chapter the chapter whose page list has to be fetched. */ - open protected fun pageListRequest(chapter: SChapter): Request { + protected open fun pageListRequest(chapter: SChapter): Request { return GET(baseUrl + chapter.url, headers) } @@ -260,7 +263,7 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - abstract protected fun pageListParse(response: Response): List + protected abstract fun pageListParse(response: Response): List /** * Returns an observable with the page containing the source url of the image. If there's any @@ -280,7 +283,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the chapter whose page list has to be fetched */ - open protected fun imageUrlRequest(page: Page): Request { + protected open fun imageUrlRequest(page: Page): Request { return GET(page.url, headers) } @@ -289,7 +292,7 @@ abstract class HttpSource : CatalogueSource { * * @param response the response from the site. */ - abstract protected fun imageUrlParse(response: Response): String + protected abstract fun imageUrlParse(response: Response): String /** * Returns an observable with the response of the source image. @@ -307,7 +310,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the chapter whose page list has to be fetched */ - open protected fun imageRequest(page: Page): Request { + protected open fun imageRequest(page: Page): Request { return GET(page.imageUrl!!, headers) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt index 8aae073e30..71ef190053 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt @@ -11,5 +11,4 @@ interface LoginSource : Source { fun login(username: String, password: String): Observable fun isAuthenticationSuccessful(response: Response): Boolean - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt index 03d58d56a2..941a3167a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt @@ -36,7 +36,7 @@ abstract class ParsedHttpSource : HttpSource() { /** * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. */ - abstract protected fun popularMangaSelector(): String + protected abstract fun popularMangaSelector(): String /** * Returns a manga from the given [element]. Most sites only show the title and the url, it's @@ -44,13 +44,13 @@ abstract class ParsedHttpSource : HttpSource() { * * @param element an element obtained from [popularMangaSelector]. */ - abstract protected fun popularMangaFromElement(element: Element): SManga + protected abstract fun popularMangaFromElement(element: Element): SManga /** * Returns the Jsoup selector that returns the tag linking to the next page, or null if * there's no next page. */ - abstract protected fun popularMangaNextPageSelector(): String? + protected abstract fun popularMangaNextPageSelector(): String? /** * Parses the response from the site and returns a [MangasPage] object. @@ -74,7 +74,7 @@ abstract class ParsedHttpSource : HttpSource() { /** * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. */ - abstract protected fun searchMangaSelector(): String + protected abstract fun searchMangaSelector(): String /** * Returns a manga from the given [element]. Most sites only show the title and the url, it's @@ -82,13 +82,13 @@ abstract class ParsedHttpSource : HttpSource() { * * @param element an element obtained from [searchMangaSelector]. */ - abstract protected fun searchMangaFromElement(element: Element): SManga + protected abstract fun searchMangaFromElement(element: Element): SManga /** * Returns the Jsoup selector that returns the tag linking to the next page, or null if * there's no next page. */ - abstract protected fun searchMangaNextPageSelector(): String? + protected abstract fun searchMangaNextPageSelector(): String? /** * Parses the response from the site and returns a [MangasPage] object. @@ -112,7 +112,7 @@ abstract class ParsedHttpSource : HttpSource() { /** * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. */ - abstract protected fun latestUpdatesSelector(): String + protected abstract fun latestUpdatesSelector(): String /** * Returns a manga from the given [element]. Most sites only show the title and the url, it's @@ -120,13 +120,13 @@ abstract class ParsedHttpSource : HttpSource() { * * @param element an element obtained from [latestUpdatesSelector]. */ - abstract protected fun latestUpdatesFromElement(element: Element): SManga + protected abstract fun latestUpdatesFromElement(element: Element): SManga /** * Returns the Jsoup selector that returns the tag linking to the next page, or null if * there's no next page. */ - abstract protected fun latestUpdatesNextPageSelector(): String? + protected abstract fun latestUpdatesNextPageSelector(): String? /** * Parses the response from the site and returns the details of a manga. @@ -142,7 +142,7 @@ abstract class ParsedHttpSource : HttpSource() { * * @param document the parsed document. */ - abstract protected fun mangaDetailsParse(document: Document): SManga + protected abstract fun mangaDetailsParse(document: Document): SManga /** * Parses the response from the site and returns a list of chapters. @@ -157,14 +157,14 @@ abstract class ParsedHttpSource : HttpSource() { /** * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter. */ - abstract protected fun chapterListSelector(): String + protected abstract fun chapterListSelector(): String /** * Returns a chapter from the given element. * * @param element an element obtained from [chapterListSelector]. */ - abstract protected fun chapterFromElement(element: Element): SChapter + protected abstract fun chapterFromElement(element: Element): SChapter /** * Parses the response from the site and returns the page list. @@ -180,7 +180,7 @@ abstract class ParsedHttpSource : HttpSource() { * * @param document the parsed document. */ - abstract protected fun pageListParse(document: Document): List + protected abstract fun pageListParse(document: Document): List /** * Parse the response from the site and returns the absolute url to the source image. @@ -196,5 +196,5 @@ abstract class ParsedHttpSource : HttpSource() { * * @param document the parsed document. */ - abstract protected fun imageUrlParse(document: Document): String + protected abstract fun imageUrlParse(document: Document): String } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/CenteredToolbar.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/CenteredToolbar.kt new file mode 100644 index 0000000000..e53602a75e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/CenteredToolbar.kt @@ -0,0 +1,31 @@ +package eu.kanade.tachiyomi.ui.base + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.graphics.drawable.DrawerArrowDrawable +import com.google.android.material.appbar.MaterialToolbar +import kotlinx.android.synthetic.main.main_activity.view.* + +class CenteredToolbar@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + MaterialToolbar(context, attrs) { + + override fun setTitle(resId: Int) { + if (navigationIcon is DrawerArrowDrawable) { + super.setTitle(resId) + toolbar_title.text = null + } else { + toolbar_title.text = context.getString(resId) + super.setTitle(null) + } + } + + override fun setTitle(title: CharSequence?) { + if (navigationIcon is DrawerArrowDrawable) { + super.setTitle(title) + toolbar_title.text = "" + } else { + toolbar_title.text = title + super.setTitle(null) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index 24d66258ad..11a2ed4f04 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -5,7 +5,11 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.main.SearchActivity +import eu.kanade.tachiyomi.ui.security.BiometricActivity +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.ThemeUtil import uy.kohesive.injekt.injectLazy abstract class BaseActivity : AppCompatActivity() { @@ -18,19 +22,20 @@ abstract class BaseActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { - AppCompatDelegate.setDefaultNightMode( - when (preferences.theme()) { - 1 -> AppCompatDelegate.MODE_NIGHT_NO - 2, 3, 4 -> AppCompatDelegate.MODE_NIGHT_YES - else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } - ) - setTheme(when (preferences.theme()) { - 3, 6 -> R.style.Theme_Tachiyomi_Amoled - 4, 7 -> R.style.Theme_Tachiyomi_DarkBlue + AppCompatDelegate.setDefaultNightMode(ThemeUtil.nightMode(preferences.theme())) + val theme = preferences.theme() + setTheme(when { + ThemeUtil.isAMOLEDTheme(theme) -> R.style.Theme_Tachiyomi_Amoled + ThemeUtil.isBlueTheme(theme) -> R.style.Theme_Tachiyomi_AllBlue else -> R.style.Theme_Tachiyomi }) super.onCreate(savedInstanceState) + SecureActivityDelegate.setSecure(this) } + override fun onResume() { + super.onResume() + if (this !is BiometricActivity && this !is SearchActivity) + SecureActivityDelegate.promptLockIfNeeded(this) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt index 7d120f5cb1..d38ecabcc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.ui.base.activity +import android.os.Bundle import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper import nucleus.view.NucleusAppCompatActivity @@ -11,4 +13,13 @@ abstract class BaseRxActivity

> : NucleusAppCompatActivity

, requestCode: Int) { val activity = activity ?: return - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - permissions.forEach { permission -> - if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) { - requestPermissions(arrayOf(permission), requestCode) - } + permissions.forEach { permission -> + if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) { + requestPermissions(arrayOf(permission), requestCode) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt index fafdbe1605..4388fa8d1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt @@ -46,7 +46,7 @@ abstract class DialogController : RestoreViewOnCreateController { dialog!!.onRestoreInstanceState(dialogState) } } - return View(activity) //stub view + return View(activity) // stub view } override fun onSaveViewState(view: View, outState: Bundle) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt deleted file mode 100644 index c036123891..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt +++ /dev/null @@ -1,3 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.controller - -interface NoToolbarElevationController \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt index def5f2eaf4..d227c1caf8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt @@ -1,8 +1,8 @@ package eu.kanade.tachiyomi.ui.base.controller import android.os.Bundle -import androidx.annotation.CallSuper import android.view.View +import androidx.annotation.CallSuper import rx.Observable import rx.Subscription import rx.subscriptions.CompositeSubscription @@ -42,7 +42,6 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) { untilDestroySubscriptions.unsubscribe() } - fun Observable.subscribeUntilDetach(): Subscription { return subscribe().also { untilDetachSubscriptions.add(it) } @@ -53,15 +52,19 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) { return subscribe(onNext).also { untilDetachSubscriptions.add(it) } } - fun Observable.subscribeUntilDetach(onNext: (T) -> Unit, - onError: (Throwable) -> Unit): Subscription { + fun Observable.subscribeUntilDetach( + onNext: (T) -> Unit, + onError: (Throwable) -> Unit + ): Subscription { return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) } } - fun Observable.subscribeUntilDetach(onNext: (T) -> Unit, - onError: (Throwable) -> Unit, - onCompleted: () -> Unit): Subscription { + fun Observable.subscribeUntilDetach( + onNext: (T) -> Unit, + onError: (Throwable) -> Unit, + onCompleted: () -> Unit + ): Subscription { return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) } } @@ -76,17 +79,20 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) { return subscribe(onNext).also { untilDestroySubscriptions.add(it) } } - fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit, - onError: (Throwable) -> Unit): Subscription { + fun Observable.subscribeUntilDestroy( + onNext: (T) -> Unit, + onError: (Throwable) -> Unit + ): Subscription { return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) } } - fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit, - onError: (Throwable) -> Unit, - onCompleted: () -> Unit): Subscription { + fun Observable.subscribeUntilDestroy( + onNext: (T) -> Unit, + onError: (Throwable) -> Unit, + onCompleted: () -> Unit + ): Subscription { return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt deleted file mode 100644 index 8a3229dd85..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.controller - -import androidx.drawerlayout.widget.DrawerLayout -import android.view.ViewGroup - -interface SecondaryDrawerController { - - fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? - - fun cleanupSecondaryDrawer(drawer: DrawerLayout) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseFlexibleViewHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseFlexibleViewHolder.kt index 2720eac37a..c4953b85d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseFlexibleViewHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseFlexibleViewHolder.kt @@ -1,17 +1,17 @@ package eu.kanade.tachiyomi.ui.base.holder import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.viewholders.FlexibleViewHolder import kotlinx.android.extensions.LayoutContainer -abstract class BaseFlexibleViewHolder(view: View, - adapter: FlexibleAdapter<*>, - stickyHeader: Boolean = false) : +abstract class BaseFlexibleViewHolder( + view: View, + adapter: FlexibleAdapter<*>, + stickyHeader: Boolean = false +) : FlexibleViewHolder(view, adapter, stickyHeader), LayoutContainer { override val containerView: View? get() = itemView -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseViewHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseViewHolder.kt index 9c0112e6da..9b03064c47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseViewHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/BaseViewHolder.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.base.holder -import androidx.recyclerview.widget.RecyclerView import android.view.View import kotlinx.android.extensions.LayoutContainer diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt index 499c4056c6..d31e6062d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt @@ -41,8 +41,13 @@ interface SlicedHolder { } } - private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, - topShadow: Boolean, bottomShadow: Boolean) { + private fun applySlice( + radius: Float, + topRect: Boolean, + bottomRect: Boolean, + topShadow: Boolean, + bottomShadow: Boolean + ) { val margin = margin slice.setRadius(radius) @@ -62,5 +67,4 @@ interface SlicedHolder { val margin get() = 8.dpToPx - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index 130362f51b..74375832a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -23,8 +23,8 @@ open class BasePresenter : RxPresenter() { * @param onNext function to execute when the observable emits an item. * @param onError function to execute when the observable throws an error. */ - fun Observable.subscribeFirst(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) - = compose(deliverFirst()).subscribe(split(onNext, onError)).apply { add(this) } + fun Observable.subscribeFirst(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = + compose(deliverFirst()).subscribe(split(onNext, onError)).apply { add(this) } /** * Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle @@ -33,8 +33,8 @@ open class BasePresenter : RxPresenter() { * @param onNext function to execute when the observable emits an item. * @param onError function to execute when the observable throws an error. */ - fun Observable.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) - = compose(deliverLatestCache()).subscribe(split(onNext, onError)).apply { add(this) } + fun Observable.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = + compose(deliverLatestCache()).subscribe(split(onNext, onError)).apply { add(this) } /** * Subscribes an observable with [deliverReplay] and adds it to the presenter's lifecycle @@ -43,8 +43,8 @@ open class BasePresenter : RxPresenter() { * @param onNext function to execute when the observable emits an item. * @param onError function to execute when the observable throws an error. */ - fun Observable.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) - = compose(deliverReplay()).subscribe(split(onNext, onError)).apply { add(this) } + fun Observable.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = + compose(deliverReplay()).subscribe(split(onNext, onError)).apply { add(this) } /** * Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle @@ -53,8 +53,8 @@ open class BasePresenter : RxPresenter() { * @param onNext function to execute when the observable emits an item. * @param onError function to execute when the observable throws an error. */ - fun Observable.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) - = compose(DeliverWithView(view())).subscribe(split(onNext, onError)).apply { add(this) } + fun Observable.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = + compose(DeliverWithView(view())).subscribe(split(onNext, onError)).apply { add(this) } /** * A deliverable that only emits to the view if attached, otherwise the event is ignored. @@ -70,5 +70,4 @@ open class BasePresenter : RxPresenter() { } } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt index bce36974bd..cd07ed478a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt @@ -43,5 +43,4 @@ class NucleusConductorDelegate

>(private val factory: PresenterF fun onDestroy() { presenter?.destroy() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt index 0e25020337..f59febccfa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt @@ -29,5 +29,4 @@ class NucleusConductorLifecycleListener(private val delegate: NucleusConductorDe companion object { private const val PRESENTER_STATE_KEY = "presenter_state" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt index 6c92007313..ca78228789 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt @@ -45,4 +45,3 @@ class CatalogueAdapter(val controller: CatalogueController) : fun onLatestClick(position: Int) } } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt index d60fe857cd..4aa257ddaa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.catalogue import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.app.Activity import android.os.Parcelable import android.view.LayoutInflater import android.view.Menu @@ -13,7 +14,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler -import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents +import com.google.android.material.bottomsheet.BottomSheetBehavior import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R @@ -26,13 +27,22 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController +import eu.kanade.tachiyomi.ui.extension.SettingsExtensionsController +import eu.kanade.tachiyomi.ui.main.BottomSheetController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.main.RootSearchInterface import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController +import eu.kanade.tachiyomi.util.view.scrollViewWith +import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.catalogue_main_controller.* +import kotlinx.android.synthetic.main.extensions_bottom_sheet.* +import kotlinx.android.synthetic.main.main_activity.* import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.math.max /** * This controller shows and manages the different catalogues enabled by the user. @@ -45,6 +55,8 @@ class CatalogueController : NucleusController(), SourceLoginDialog.Listener, FlexibleAdapter.OnItemClickListener, CatalogueAdapter.OnBrowseClickListener, + RootSearchInterface, + BottomSheetController, CatalogueAdapter.OnLatestClickListener { /** @@ -57,6 +69,13 @@ class CatalogueController : NucleusController(), */ private var adapter: CatalogueAdapter? = null + var extQuery = "" + private set + + var headerHeight = 0 + + var showingExtenions = false + /** * Called when controller is initialized. */ @@ -71,7 +90,9 @@ class CatalogueController : NucleusController(), * @return title. */ override fun getTitle(): String? { - return applicationContext?.getString(R.string.label_catalogues) + return if (showingExtenions) + applicationContext?.getString(R.string.extensions) + else applicationContext?.getString(R.string.sources) } /** @@ -101,6 +122,7 @@ class CatalogueController : NucleusController(), */ override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForRootController(activity!!.bottom_nav) adapter = CatalogueAdapter(this) @@ -108,9 +130,78 @@ class CatalogueController : NucleusController(), recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context) recycler.adapter = adapter recycler.addItemDecoration(SourceDividerItemDecoration(view.context)) - recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = view.context.obtainStyledAttributes(attrsArray) + val appBarHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + scrollViewWith(recycler) { + headerHeight = it.systemWindowInsetTop + appBarHeight + } requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) + ext_bottom_sheet.onCreate(this) + + ext_bottom_sheet.sheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior + .BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { + shadow2.alpha = (1 - max(0f, progress)) * 0.25f + activity?.appbar?.elevation = max(progress * 15f, + if (recycler.canScrollVertically(-1)) 15f else 0f) + + sheet_layout.alpha = 1 - progress + activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress)) + val oldShow = showingExtenions + showingExtenions = progress > 0.92f + if (oldShow != showingExtenions) { + setTitle() + activity?.invalidateOptionsMenu() + } + } + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) activity?.appbar?.y = 0f + if (state == BottomSheetBehavior.STATE_EXPANDED || + state == BottomSheetBehavior.STATE_COLLAPSED) { + sheet_layout.alpha = + if (state == BottomSheetBehavior.STATE_COLLAPSED) 1f else 0f + showingExtenions = state == BottomSheetBehavior.STATE_EXPANDED + setTitle() + if (state == BottomSheetBehavior.STATE_EXPANDED) + ext_bottom_sheet.fetchOnlineExtensionsIfNeeded() + else ext_bottom_sheet.shouldCallApi = true + activity?.invalidateOptionsMenu() + } + + retainViewMode = if (state == BottomSheetBehavior.STATE_EXPANDED) + RetainViewMode.RETAIN_DETACH else RetainViewMode.RELEASE_DETACH + sheet_layout.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED + sheet_layout.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED + } + }) + + if (showingExtenions) { + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun showSheet() { + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun toggleSheet() { + if (ext_bottom_sheet.sheetBehavior?.state != BottomSheetBehavior.STATE_COLLAPSED) { + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } else { + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun handleSheetBack(): Boolean { + if (ext_bottom_sheet.sheetBehavior?.state != BottomSheetBehavior.STATE_COLLAPSED) { + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + return true + } + return false } override fun onDestroyView(view: View) { @@ -121,10 +212,17 @@ class CatalogueController : NucleusController(), override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeStarted(handler, type) if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) { - presenter.updateSources() + view?.applyWindowInsetsForRootController(activity!!.bottom_nav) + ext_bottom_sheet.updateExtTitle() + ext_bottom_sheet.presenter.refreshExtensions() } } + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + ext_bottom_sheet?.presenter?.refreshExtensions() + } + /** * Called when login dialog is closed, refreshes the adapter. * @@ -177,6 +275,12 @@ class CatalogueController : NucleusController(), router.pushController(controller.withFadeTransaction()) } + override fun expandSearch() { + if (showingExtenions) + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + else activity?.toolbar?.menu?.findItem(R.id.action_search)?.expandActionView() + } + /** * Adds items to the options menu. * @@ -184,23 +288,44 @@ class CatalogueController : NucleusController(), * @param inflater used to load the menu xml. */ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - // Inflate menu - inflater.inflate(R.menu.catalogue_main, menu) + if (onRoot) (activity as? MainActivity)?.setDismissIcon(showingExtenions) + if (showingExtenions) { + // Inflate menu + inflater.inflate(R.menu.extension_main, menu) + + // Initialize search option. + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + + // Change hint to show global search. + searchView.queryHint = applicationContext?.getString(R.string.search_extensions) + + // Create query listener which opens the global search view. + setOnQueryTextChangeListener(searchView) { + extQuery = it ?: "" + ext_bottom_sheet.drawExtensions() + true + } + } else { + // Inflate menu + inflater.inflate(R.menu.catalogue_main, menu) - // Initialize search option. - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView + // Initialize search option. + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView - // Change hint to show global search. - searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint) + // Change hint to show global search. + searchView.queryHint = applicationContext?.getString(R.string.global_search) - // Create query listener which opens the global search view. - searchView.queryTextChangeEvents() - .filter { it.isSubmitted } - .subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) } + // Create query listener which opens the global search view. + setOnQueryTextChangeListener(searchView, true) { + if (!it.isNullOrBlank()) performGlobalSearch(it) + true + } + } } - private fun performGlobalSearch(query: String){ + private fun performGlobalSearch(query: String) { router.pushController(CatalogueSearchController(query).withFadeTransaction()) } @@ -213,10 +338,16 @@ class CatalogueController : NucleusController(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { // Initialize option to open catalogue settings. - R.id.action_settings -> { - router.pushController((RouterTransaction.with(SettingsSourcesController())) - .popChangeHandler(SettingsSourcesFadeChangeHandler()) - .pushChangeHandler(FadeChangeHandler())) + R.id.action_filter -> { + val controller = + if (showingExtenions) + SettingsExtensionsController() + else SettingsSourcesController() + router.pushController( + (RouterTransaction.with(controller)).popChangeHandler( + SettingsSourcesFadeChangeHandler() + ).pushChangeHandler(FadeChangeHandler()) + ) } else -> return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 096c5b19e7..95f3acdbf9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -12,7 +12,7 @@ import rx.Subscription import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.* +import java.util.TreeMap import java.util.concurrent.TimeUnit /** @@ -23,8 +23,8 @@ import java.util.concurrent.TimeUnit * @param preferences application preferences. */ class CataloguePresenter( - val sourceManager: SourceManager = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get() + val sourceManager: SourceManager = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() ) : BasePresenter() { /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt index 2d6dc40e53..92ac8926ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt @@ -1,12 +1,12 @@ package eu.kanade.tachiyomi.ui.catalogue import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.system.LocaleHelper -import kotlinx.android.synthetic.main.catalogue_main_controller_card.title +import kotlinx.android.synthetic.main.catalogue_main_controller_card.* class LangHolder(view: View, adapter: FlexibleAdapter>) : BaseFlexibleViewHolder(view, adapter) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt index c3f15a3f50..8e3167f358 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt @@ -34,5 +34,4 @@ data class LangItem(val code: String) : AbstractHeaderItem() { override fun bindViewHolder(adapter: FlexibleAdapter>, holder: LangHolder, position: Int, payloads: MutableList) { holder.bind(this) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt index 0475b90276..a268f29e43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt @@ -4,7 +4,6 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.Drawable -import androidx.recyclerview.widget.RecyclerView import android.view.View class SourceDividerItemDecoration(context: Context) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() { @@ -36,9 +35,12 @@ class SourceDividerItemDecoration(context: Context) : androidx.recyclerview.widg } } - override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView, - state: androidx.recyclerview.widget.RecyclerView.State) { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: androidx.recyclerview.widget.RecyclerView, + state: androidx.recyclerview.widget.RecyclerView.State + ) { outRect.set(0, 0, 0, divider.intrinsicHeight) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt index f29fff143c..3245948790 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt @@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.ui.catalogue import android.view.View import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder -import eu.kanade.tachiyomi.util.view.getRound import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.roundTextIcon import eu.kanade.tachiyomi.util.view.visible import io.github.mthli.slice.Slice import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.* @@ -41,7 +42,9 @@ class SourceHolder(view: View, override val adapter: CatalogueAdapter) : // Set circle letter image. itemView.post { - image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(), false)) + val icon = source.icon() + if (icon != null) edit_button.setImageDrawable(source.icon()) + else edit_button.roundTextIcon(source.name) } // If source is login, show only login option diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt index 830f82b891..6bd24406e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt @@ -37,5 +37,4 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) override fun bindViewHolder(adapter: FlexibleAdapter>, holder: SourceHolder, position: Int, payloads: MutableList) { holder.bind(this) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt index 6f5dd4d44a..e33cf9cb3d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.catalogue.browse -import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -9,11 +8,9 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SearchView -import androidx.core.view.GravityCompat import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.f2prateek.rx.preferences.Preference import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents @@ -23,31 +20,29 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.HeightTopWindowInsetsListener import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets +import eu.kanade.tachiyomi.util.system.connectivityManager +import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.util.view.marginTop +import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.view.visible -import eu.kanade.tachiyomi.util.system.connectivityManager +import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.android.synthetic.main.catalogue_controller.* -import kotlinx.android.synthetic.main.main_activity.* import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -60,18 +55,19 @@ import java.util.concurrent.TimeUnit */ open class BrowseCatalogueController(bundle: Bundle) : NucleusController(bundle), - SecondaryDrawerController, FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.EndlessScrollListener, ChangeMangaCategoriesDialog.Listener { - constructor(source: CatalogueSource, + constructor( + source: CatalogueSource, searchQuery: String? = null, - smartSearchConfig: CatalogueController.SmartSearchConfig? = null) : this(Bundle().apply { + smartSearchConfig: CatalogueController.SmartSearchConfig? = null + ) : this(Bundle().apply { putLong(SOURCE_ID_KEY, source.id) - if(searchQuery != null) + if (searchQuery != null) putString(SEARCH_QUERY_KEY, searchQuery) if (smartSearchConfig != null) @@ -105,18 +101,13 @@ open class BrowseCatalogueController(bundle: Bundle) : /** * Recycler view with the list of results. */ - private var recycler: androidx.recyclerview.widget.RecyclerView? = null + private var recycler: RecyclerView? = null /** * Subscription for the search view. */ private var searchViewSubscription: Subscription? = null - /** - * Subscription for the number of manga per row. - */ - private var numColumnsSubscription: Subscription? = null - /** * Endless loading item. */ @@ -147,12 +138,12 @@ open class BrowseCatalogueController(bundle: Bundle) : navView?.setFilters(presenter.filterItems) + fab.visibleIf(presenter.sourceFilters.isNotEmpty()) + fab.setOnClickListener { showFilters() } progress?.visible() } override fun onDestroyView(view: View) { - numColumnsSubscription?.unsubscribe() - numColumnsSubscription = null searchViewSubscription?.unsubscribe() searchViewSubscription = null adapter = null @@ -161,58 +152,10 @@ open class BrowseCatalogueController(bundle: Bundle) : super.onDestroyView(view) } - override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? { - // Inflate and prepare drawer - val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView - this.navView = navView - navView.setFilters(presenter.filterItems) - - drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - - navView.onSearchClicked = { - val allDefault = presenter.sourceFilters == presenter.source.getFilterList() - showProgressBar() - adapter?.clear() - drawer.closeDrawer(GravityCompat.END) - presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) - } - - navView.onResetClicked = { - presenter.appliedFilters = FilterList() - val newFilters = presenter.source.getFilterList() - presenter.sourceFilters = newFilters - navView.setFilters(presenter.filterItems) - } - drawer.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - - val statusScrim = navView.findViewById(R.id.status_bar_scrim) as View - statusScrim.setOnApplyWindowInsetsListener(HeightTopWindowInsetsListener) - val titleView = navView.findViewById(R.id.title_background) as View - val titleMarginTop = titleView.marginTop - navView.doOnApplyWindowInsets { v, insets, padding -> - v.updatePaddingRelative( - bottom = padding.bottom + insets.systemWindowInsetBottom, - end = padding.right + insets.systemWindowInsetRight - ) - titleView.updateLayoutParams { - topMargin = titleMarginTop + insets.systemWindowInsetTop - } - } - return navView - } - - override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) { - navView = null - } - private fun setupRecycler(view: View) { - numColumnsSubscription?.unsubscribe() - - var oldPosition = androidx.recyclerview.widget.RecyclerView.NO_POSITION + var oldPosition = RecyclerView.NO_POSITION val oldRecycler = catalogue_view?.getChildAt(1) - if (oldRecycler is androidx.recyclerview.widget.RecyclerView) { + if (oldRecycler is RecyclerView) { oldPosition = (oldRecycler.layoutManager as androidx.recyclerview.widget.LinearLayoutManager).findFirstVisibleItemPosition() oldRecycler.adapter = null @@ -228,11 +171,11 @@ open class BrowseCatalogueController(bundle: Bundle) : } } else { (catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { - numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() - .doOnNext { spanCount = it } - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribe { adapter = this@BrowseCatalogueController.adapter } + columnWidth = when (preferences.gridSize().getOrDefault()) { + 0 -> 1f + 2 -> 1.66f + else -> 1.25f + } (layoutManager as androidx.recyclerview.widget.GridLayoutManager).spanSizeLookup = object : androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { @@ -244,12 +187,27 @@ open class BrowseCatalogueController(bundle: Bundle) : } } } + recycler.clipToPadding = false recycler.setHasFixedSize(true) recycler.adapter = adapter + scrollViewWith(recycler, true) { insets -> + fab.updateLayoutParams { + bottomMargin = insets.systemWindowInsetBottom + 16.dpToPx + } + } + + recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (dy <= 0) + fab.extend() + else + fab.shrink() + } + }) + catalogue_view.addView(recycler, 1) - recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - if (oldPosition != androidx.recyclerview.widget.RecyclerView.NO_POSITION) { + if (oldPosition != RecyclerView.NO_POSITION) { recycler.layoutManager?.scrollToPosition(oldPosition) } this.recycler = recycler @@ -292,18 +250,6 @@ open class BrowseCatalogueController(bundle: Bundle) : } ) - // Setup filters button - menu.findItem(R.id.action_set_filter).apply { - icon.mutate() - if (presenter.sourceFilters.isEmpty()) { - isEnabled = false - icon.alpha = 128 - } else { - isEnabled = true - icon.alpha = 255 - } - } - // Show next display mode menu.findItem(R.id.action_display_mode).apply { val icon = if (presenter.isListMode) @@ -325,13 +271,63 @@ open class BrowseCatalogueController(bundle: Bundle) : when (item.itemId) { R.id.action_search -> expandActionViewFromInteraction = true R.id.action_display_mode -> swapDisplayMode() - R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) } R.id.action_open_in_web_view -> openInWebView() else -> return super.onOptionsItemSelected(item) } return true } + private fun showFilters() { + val sheet = CatalogueSearchSheet(activity!!) + sheet.setFilters(presenter.filterItems) + presenter.filtersChanged = false + val oldFilters = mutableListOf() + for (i in presenter.sourceFilters) { + if (i is Filter.Group<*>) { + val subFilters = mutableListOf() + for (j in i.state) { + subFilters.add((j as Filter<*>).state) + } + oldFilters.add(subFilters) + } else { + oldFilters.add(i.state) + } + } + sheet.onSearchClicked = { + var matches = true + for (i in presenter.sourceFilters.indices) { + val filter = oldFilters[i] + if (filter is List<*>) { + for (j in filter.indices) { + if (filter[j] != + ((presenter.sourceFilters[i] as Filter.Group<*>).state[j] as + Filter<*>).state) { + matches = false + break + } + } + } else if (oldFilters[i] != presenter.sourceFilters[i].state) { + matches = false + break + } + } + if (!matches) { + val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + showProgressBar() + adapter?.clear() + presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) + } + } + + sheet.onResetClicked = { + presenter.appliedFilters = FilterList() + val newFilters = presenter.source.getFilterList() + presenter.sourceFilters = newFilters + sheet.setFilters(presenter.filterItems) + } + sheet.show() + } + private fun openInWebView() { val source = presenter.source as? HttpSource ?: return val activity = activity ?: return @@ -386,7 +382,7 @@ open class BrowseCatalogueController(bundle: Bundle) : snack?.dismiss() val message = if (error is NoResultsException) catalogue_view.context.getString(R.string.no_results_found) else (error.message ?: "") snack = catalouge_layout?.snack(message, Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_retry) { + setAction(R.string.retry) { // If not the first page, show bottom progress bar. if (adapter.mainItemCount > 0) { val item = progressItem ?: return@setAction @@ -452,18 +448,6 @@ open class BrowseCatalogueController(bundle: Bundle) : } } - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) - preferences.portraitColumns() - else - preferences.landscapeColumns() - } - /** * Returns the view holder for the given manga. * @@ -507,7 +491,7 @@ open class BrowseCatalogueController(bundle: Bundle) : */ override fun onItemClick(view: View?, position: Int): Boolean { val item = adapter?.getItem(position) as? CatalogueItem ?: return false - router.pushController(MangaController(item.manga, true).withFadeTransaction()) + router.pushController(MangaDetailsController(item.manga, true).withFadeTransaction()) return false } @@ -527,8 +511,8 @@ open class BrowseCatalogueController(bundle: Bundle) : if (manga.favorite) { presenter.changeMangaFavorite(manga) adapter?.notifyItemChanged(position) - snack = catalouge_layout?.snack(R.string.manga_removed_library, Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_undo) { + snack = catalouge_layout?.snack(R.string.removed_from_library, Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.undo) { if (!manga.favorite) addManga(manga, position) } addCallback(object : BaseTransientBottomBar.BaseCallback() { @@ -541,7 +525,7 @@ open class BrowseCatalogueController(bundle: Bundle) : (activity as? MainActivity)?.setUndoSnackBar(snack) } else { addManga(manga, position) - snack = catalouge_layout?.snack(R.string.manga_added_library) + snack = catalouge_layout?.snack(R.string.added_to_library) } } @@ -588,5 +572,4 @@ open class BrowseCatalogueController(bundle: Bundle) : const val SEARCH_QUERY_KEY = "searchQuery" const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt index d93c36e924..3cc8fee9d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt @@ -16,7 +16,19 @@ import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.catalogue.filter.* +import eu.kanade.tachiyomi.ui.catalogue.filter.CheckboxItem +import eu.kanade.tachiyomi.ui.catalogue.filter.CheckboxSectionItem +import eu.kanade.tachiyomi.ui.catalogue.filter.GroupItem +import eu.kanade.tachiyomi.ui.catalogue.filter.HeaderItem +import eu.kanade.tachiyomi.ui.catalogue.filter.SelectItem +import eu.kanade.tachiyomi.ui.catalogue.filter.SelectSectionItem +import eu.kanade.tachiyomi.ui.catalogue.filter.SeparatorItem +import eu.kanade.tachiyomi.ui.catalogue.filter.SortGroup +import eu.kanade.tachiyomi.ui.catalogue.filter.SortItem +import eu.kanade.tachiyomi.ui.catalogue.filter.TextItem +import eu.kanade.tachiyomi.ui.catalogue.filter.TextSectionItem +import eu.kanade.tachiyomi.ui.catalogue.filter.TriStateItem +import eu.kanade.tachiyomi.ui.catalogue.filter.TriStateSectionItem import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -25,16 +37,17 @@ import rx.subjects.PublishSubject import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Date /** * Presenter of [BrowseCatalogueController]. */ open class BrowseCataloguePresenter( - sourceId: Long, - sourceManager: SourceManager = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val prefs: PreferencesHelper = Injekt.get(), - private val coverCache: CoverCache = Injekt.get() + sourceId: Long, + sourceManager: SourceManager = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val prefs: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get() ) : BasePresenter() { /** @@ -48,12 +61,15 @@ open class BrowseCataloguePresenter( var query = "" private set + var filtersChanged = false + /** * Modifiable list of filters. */ var sourceFilters = FilterList() set(value) { field = value + filtersChanged = true filterItems = value.toItems() } @@ -133,6 +149,7 @@ open class BrowseCataloguePresenter( val sourceId = source.id val catalogueAsList = prefs.catalogueAsList() + val catalougeListType = prefs.libraryLayout() // Prepare the pager. pagerSubscription?.let { remove(it) } @@ -140,7 +157,7 @@ open class BrowseCataloguePresenter( .observeOn(Schedulers.io()) .map { it.first to it.second.map { networkToLocalManga(it, sourceId) } } .doOnNext { initializeMangas(it.second) } - .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } } + .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList, catalougeListType) } } .observeOn(AndroidSchedulers.mainThread()) .subscribeReplay({ view, (page, mangas) -> view.onAddPage(page, mangas) @@ -217,8 +234,7 @@ open class BrowseCataloguePresenter( val result = db.insertManga(newManga).executeAsBlocking() newManga.id = result.insertedId() localManga = newManga - } - else if (localManga.title.isBlank()) { + } else if (localManga.title.isBlank()) { localManga.title = sManga.title db.insertManga(localManga).executeAsBlocking() } @@ -258,13 +274,19 @@ open class BrowseCataloguePresenter( */ fun changeMangaFavorite(manga: Manga) { manga.favorite = !manga.favorite + + when (manga.favorite) { + true -> manga.date_added = Date().time + false -> manga.date_added = 0 + } + db.insertManga(manga).executeAsBlocking() } fun confirmDeletion(manga: Manga) { coverCache.deleteFromCache(manga.thumbnail_url) val downloadManager: DownloadManager = Injekt.get() - downloadManager.deleteManga(manga,source) + downloadManager.deleteManga(manga, source) db.resetMangaInfo(manga).executeAsBlocking() } @@ -381,5 +403,4 @@ open class BrowseCataloguePresenter( changeMangaFavorite(manga) } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt index 0fb957f2cc..3853bc8edf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt @@ -1,51 +1,71 @@ package eu.kanade.tachiyomi.ui.catalogue.browse import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter +import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.widget.StateImageViewTarget import kotlinx.android.synthetic.main.catalogue_grid_item.* -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.items.IFlexible +import kotlinx.android.synthetic.main.unread_download_badge.* /** - * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. + * Class used to hold the displayed data of a manga in the library, like the cover or the title. * All the elements from the layout file "item_catalogue_grid" 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. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. */ -class CatalogueGridHolder(private val view: View, private val adapter: FlexibleAdapter>) : - CatalogueHolder(view, adapter) { +class CatalogueGridHolder( + private val view: View, + private val adapter: FlexibleAdapter>, + compact: Boolean +) : CatalogueHolder(view, adapter) { + + init { + if (compact) { + text_layout.gone() + } else { + compact_title.gone() + gradient.gone() + } + } /** - * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. * - * @param manga the manga to bind. + * @param manga the manga item to bind. */ override fun onSetValues(manga: Manga) { - // Set manga title - title.text = manga.originalTitle() - - // Set alpha of thumbnail. - thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f + // Update the title of the manga. + title.text = manga.title + compact_title.text = title.text + badge_view.setInLibrary(manga.favorite) + // Update the cover. setImage(manga) } override fun setImage(manga: Manga) { - GlideApp.with(view.context).clear(thumbnail) - if (!manga.thumbnail_url.isNullOrEmpty()) { + if (manga.thumbnail_url == null) + Glide.with(view.context).clear(cover_thumbnail) + else { GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .centerCrop() - .placeholder(android.R.color.transparent) - .into(StateImageViewTarget(thumbnail, progress)) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .centerCrop() + .placeholder(android.R.color.transparent) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(StateImageViewTarget(cover_thumbnail, progress)) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt index 2356d1686f..722c731668 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.ui.catalogue.browse import android.view.View +import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.items.IFlexible /** * Generic class used to hold the displayed data of a manga in the catalogue. @@ -24,7 +24,6 @@ abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter) : +class CatalogueItem( + val manga: Manga, + private val catalogueAsList: Preference, + private val catalogueListType: Preference +) : AbstractFlexibleItem() { override fun getLayoutRes(): Int { @@ -28,22 +37,47 @@ class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference>): CatalogueHolder { val parent = adapter.recyclerView return if (parent is AutofitRecyclerView) { + val listType = catalogueListType.getOrDefault() view.apply { - card.layoutParams = FrameLayout.LayoutParams( - MATCH_PARENT, parent.itemWidth / 3 * 4) - gradient.layoutParams = FrameLayout.LayoutParams( - MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM) + val coverHeight = (parent.itemWidth / 3 * 4f).toInt() + if (listType == 1) { + gradient.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + (coverHeight * 0.66f).toInt(), + Gravity.BOTTOM) + card.updateLayoutParams { + bottomMargin = 6.dpToPx + } + } else { + constraint_layout.background = ContextCompat.getDrawable( + context, R.drawable.library_item_selector + ) + } + constraint_layout.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + cover_thumbnail.maxHeight = Int.MAX_VALUE + cover_thumbnail.minimumHeight = 0 + constraint_layout.minHeight = 0 + cover_thumbnail.scaleType = ImageView.ScaleType.CENTER_CROP + cover_thumbnail.adjustViewBounds = false + cover_thumbnail.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + (parent.itemWidth / 3f * 3.7f).toInt() + ) } - CatalogueGridHolder(view, adapter) + CatalogueGridHolder(view, adapter, listType == 1) } else { CatalogueListHolder(view, adapter) } } - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: CatalogueHolder, - position: Int, - payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: CatalogueHolder, + position: Int, + payloads: MutableList? + ) { holder.onSetValues(manga) } @@ -59,5 +93,4 @@ class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference>) : CatalogueHolder(view, adapter) { - private val favoriteColor = view.context.getResourceColor(android.R.attr.textColorHint) - private val unfavoriteColor = view.context.getResourceColor(android.R.attr.textColorPrimary) - /** * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. @@ -32,24 +31,27 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter> = FlexibleAdapter>(null) .setDisplayHeadersAtStartUp(true) @@ -28,13 +26,16 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: val view = inflate(R.layout.catalogue_drawer_content) ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler) addView(view) - title.text = context.getString(R.string.source_search_options) - search_btn.setOnClickListener { onSearchClicked() } + // title.text = context.getString(R.string.source_search_options) + /*search_btn.setOnClickListener { onSearchClicked() } reset_btn.setOnClickListener { onResetClicked() } + view.search_layout.setOnApplyWindowInsetsListener { v, insets -> + view.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + insets + }*/ } fun setFilters(items: List>) { adapter.updateDataSet(items) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt index a7b563074a..08dad01c28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt @@ -28,5 +28,4 @@ open class CataloguePager(val source: CatalogueSource, val query: String, val fi } } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueSearchSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueSearchSheet.kt new file mode 100644 index 0000000000..d3d25b31be --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueSearchSheet.kt @@ -0,0 +1,116 @@ +package eu.kanade.tachiyomi.ui.catalogue.browse + +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.app.Activity +import android.os.Build +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +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.getOrDefault +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import kotlinx.android.synthetic.main.catalogue_drawer_content.* +import uy.kohesive.injekt.injectLazy + +class CatalogueSearchSheet(activity: Activity) : + BottomSheetDialog(activity, R.style.BottomSheetDialogTheme) { + + /** + * Preferences helper. + */ + private val preferences by injectLazy() + + private var sheetBehavior: BottomSheetBehavior<*> + + private var elevationAnimator: ValueAnimator? = null + + var filterChanged = true + + var isNotElevated = false + + val adapter: FlexibleAdapter> = FlexibleAdapter>(null) + .setDisplayHeadersAtStartUp(true) + + var onSearchClicked = {} + + var onResetClicked = {} + + init { + val view = activity.layoutInflater.inflate(R.layout.catalogue_drawer_content, null) + setContentView(view) + toolbar_title.text = context.getString(R.string.search_filters) + dismiss_button.setOnClickListener { dismiss() } + reset_btn.setOnClickListener { onResetClicked() } + /*view.search_layout.setOnApplyWindowInsetsListener { v, insets -> + view.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + insets + }*/ + + recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + recycler.clipToPadding = false + recycler.adapter = adapter + recycler.setHasFixedSize(true) + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) + sheetBehavior.skipCollapsed = true + sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + setEdgeToEdge( + activity, view, 50.dpToPx + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && preferences.readerTheme() + .getOrDefault() == 0 && activity.window.decorView.rootWindowInsets.systemWindowInsetRight == 0 && activity.window.decorView.rootWindowInsets.systemWindowInsetLeft == 0 + ) window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + val height = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + + sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) {} + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.skipCollapsed = true + } + } + }) + + recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val atTop = !recycler.canScrollVertically(-1) + if (atTop != isNotElevated) { + elevationAnimator?.cancel() + isNotElevated = atTop + elevationAnimator?.cancel() + elevationAnimator = ObjectAnimator.ofFloat( + title_layout, + "elevation", + title_layout.elevation, + if (atTop) 0f else 10f.dpToPx + ) + elevationAnimator?.duration = 100 + elevationAnimator?.start() + } + } + }) + } + + override fun onStart() { + super.onStart() + sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun dismiss() { + super.dismiss() + if (filterChanged) + onSearchClicked() + } + + fun setFilters(items: List>) { + adapter.updateDataSet(items) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt index 723782f5e5..ae6f39bcda 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt @@ -1,3 +1,3 @@ package eu.kanade.tachiyomi.ui.catalogue.browse -class NoResultsException : Exception() \ No newline at end of file +class NoResultsException : Exception() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt index 104e5887df..62b88aa855 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt @@ -27,5 +27,4 @@ abstract class Pager(var currentPage: Int = 1) { hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty() results.call(Pair(page, mangasPage.mangas)) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt index 3080240307..398df17b73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt @@ -10,7 +10,6 @@ import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R - class ProgressItem : AbstractFlexibleItem() { private var loadMore = true @@ -47,5 +46,4 @@ class ProgressItem : AbstractFlexibleItem() { val progressBar: ProgressBar = view.findViewById(R.id.progress_bar) val progressMessage: TextView = view.findViewById(R.id.progress_message) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt index a54f6c0edd..d252cb3f03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt @@ -40,7 +40,6 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem) : AbstractExpandableHeaderItem>) : ExpandableViewHolder(view, adapter, true) { val title: TextView = itemView.findViewById(R.id.title) @@ -62,6 +60,5 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem() { @SuppressLint("PrivateResource") override fun getLayoutRes(): Int { - return R.layout.design_navigation_item_subheader + return com.google.android.material.R.layout.design_navigation_item_subheader } override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { @@ -25,6 +25,7 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem>, holder: Holder, position: Int, payloads: MutableList?) { val view = holder.itemView as TextView view.text = filter.name + view.setTextColor(view.context.getResourceColor(android.R.attr.textColorPrimary)) } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt index 188d92cc4e..5de9d4176d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.ui.catalogue.filter import android.annotation.SuppressLint -import com.google.android.material.R import android.view.View import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.R import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.IFlexible @@ -21,9 +21,12 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem>, - holder: Holder, position: Int, payloads: MutableList?) { - + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: Holder, + position: Int, + payloads: MutableList? + ) { } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt index 6c7f867dc9..1e65a3fd26 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt @@ -37,7 +37,6 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem>, holder: CatalogueSearchCardHolder, - position: Int, payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: CatalogueSearchCardHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(manga) } @@ -33,5 +37,4 @@ class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem(), CatalogueSearchCardAdapter.OnMangaClickListener { @@ -34,7 +35,7 @@ open class CatalogueSearchController( */ protected var adapter: CatalogueSearchAdapter? = null - private var customTitle:String? = null + private var customTitle: String? = null /** * Called when controller is initialized. @@ -79,7 +80,7 @@ open class CatalogueSearchController( */ override fun onMangaClick(manga: Manga) { // Open MangaController. - router.pushController(MangaController(manga, true).withFadeTransaction()) + router.pushController(MangaDetailsController(manga, true).withFadeTransaction()) } /** @@ -135,6 +136,7 @@ open class CatalogueSearchController( */ override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForController() adapter = CatalogueSearchAdapter(this) @@ -142,8 +144,7 @@ open class CatalogueSearchController( recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context) recycler.adapter = adapter recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - if (extensionFilter != null) - { + if (extensionFilter != null) { customTitle = view.context?.getString(R.string.loading) setTitle() } @@ -193,12 +194,11 @@ open class CatalogueSearchController( val results = searchResult.first().results if (results != null && results.size == 1) { val manga = results.first().manga - router.replaceTopController(MangaController(manga,true,fromExtension = true) + router.replaceTopController(MangaDetailsController(manga, true) .withFadeTransaction() ) return - } - else if (results != null) { + } else if (results != null) { customTitle = null setTitle() activity?.invalidateOptionsMenu() @@ -215,5 +215,4 @@ open class CatalogueSearchController( fun onMangaInitialized(source: CatalogueSource, manga: Manga) { getHolder(source)?.setImage(manga) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt index 678b9a8be1..ce878490bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt @@ -1,16 +1,12 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search -import androidx.recyclerview.widget.LinearLayoutManager import android.view.View -import eu.kanade.tachiyomi.R +import androidx.recyclerview.widget.LinearLayoutManager import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.visible -import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.progress -import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.recycler -import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.source_card -import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.title +import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.* /** * Holder that binds the [CatalogueSearchItem] containing catalogue cards. @@ -105,5 +101,4 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : title.gone() source_card.gone() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt index 54796d3f98..c8408f2109 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt @@ -15,8 +15,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource * @param results the search results. * @param highlighted whether this search item should be highlighted/marked in the catalogue search view. */ -class CatalogueSearchItem(val source: CatalogueSource, val results: List?, val highlighted: Boolean = false) - : AbstractFlexibleItem() { +class CatalogueSearchItem(val source: CatalogueSource, val results: List?, val highlighted: Boolean = false) : + AbstractFlexibleItem() { /** * Set view. @@ -39,8 +39,12 @@ class CatalogueSearchItem(val source: CatalogueSource, val results: List>, holder: CatalogueSearchHolder, - position: Int, payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: CatalogueSearchHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(this) } @@ -64,5 +68,4 @@ class CatalogueSearchItem(val source: CatalogueSource, val results: List() { /** @@ -74,11 +74,13 @@ open class CatalogueSearchPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - extensionFilter = savedState?.getString(CatalogueSearchPresenter::extensionFilter.name) ?: - initialExtensionFilter + extensionFilter = savedState?.getString(CatalogueSearchPresenter::extensionFilter.name) + ?: initialExtensionFilter // Perform a search with previous or initial state - search(savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty()) + search( + savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty() + ) } override fun onDestroy() { @@ -102,11 +104,10 @@ open class CatalogueSearchPresenter( val languages = preferencesHelper.enabledLanguages().getOrDefault() val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault() - return sourceManager.getCatalogueSources() - .filter { it.lang in languages } - .filterNot { it is LoginSource && !it.isLogged() } - .filterNot { it.id.toString() in hiddenCatalogues } - .sortedBy { "(${it.lang}) ${it.name}" } + return sourceManager.getCatalogueSources().filter { it.lang in languages } + .filterNot { it is LoginSource && !it.isLogged() } + .filterNot { it.id.toString() in hiddenCatalogues } + .sortedBy { "(${it.lang}) ${it.name}" } } private fun getSourcesToQuery(): List { @@ -116,10 +117,8 @@ open class CatalogueSearchPresenter( return enabledSources } - val filterSources = extensionManager.installedExtensions - .filter { it.pkgName == filter } - .flatMap { it.sources } - .filter { it in enabledSources } + val filterSources = extensionManager.installedExtensions.filter { it.pkgName == filter } + .flatMap { it.sources }.filter { it in enabledSources } .filterIsInstance() if (filterSources.isEmpty()) { @@ -132,7 +131,10 @@ open class CatalogueSearchPresenter( /** * Creates a catalogue search item */ - protected open fun createCatalogueSearchItem(source: CatalogueSource, results: List?): CatalogueSearchItem { + protected open fun createCatalogueSearchItem( + source: CatalogueSource, + results: List? + ): CatalogueSearchItem { return CatalogueSearchItem(source, results) } @@ -156,30 +158,42 @@ open class CatalogueSearchPresenter( var items = initialItems fetchSourcesSubscription?.unsubscribe() - fetchSourcesSubscription = Observable.from(sources) - .flatMap({ source -> - Observable.defer { source.fetchSearchManga(1, query, FilterList()) } - .subscribeOn(Schedulers.io()) - .onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions - .map { it.mangas.take(10) } // Get at most 10 manga from search result. - .map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga. - .doOnNext { fetchImage(it, source) } // Load manga covers. - .map { createCatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) } - }, 5) - .observeOn(AndroidSchedulers.mainThread()) - // Update matching source with the obtained results - .map { result -> - items.map { item -> if (item.source == result.source) result else item } - } - // Update current state - .doOnNext { items = it } - // Deliver initial state - .startWith(initialItems) - .subscribeLatestCache({ view, manga -> - view.setItems(manga) - }, { _, error -> - Timber.e(error) - }) + fetchSourcesSubscription = Observable.from(sources).flatMap({ source -> + Observable.defer { source.fetchSearchManga(1, query, FilterList()) } + .subscribeOn(Schedulers.io()).onErrorReturn { + MangasPage( + emptyList(), + false + ) + } // Ignore timeouts or other exceptions + .map { it.mangas.take(10) } // Get at most 10 manga from search result. + .map { + it.map { + networkToLocalManga( + it, + source.id + ) + } + } // Convert to local manga. + .doOnNext { fetchImage(it, source) } // Load manga covers. + .map { + createCatalogueSearchItem( + source, + it.map { CatalogueSearchCardItem(it) }) + } + }, 5).observeOn(AndroidSchedulers.mainThread()) + // Update matching source with the obtained results + .map { result -> + items.map { item -> if (item.source == result.source) result else item } + } + // Update current state + .doOnNext { items = it } + // Deliver initial state + .startWith(initialItems).subscribeLatestCache({ view, manga -> + view.setItems(manga) + }, { _, error -> + Timber.e(error) + }) } /** @@ -196,23 +210,18 @@ open class CatalogueSearchPresenter( */ private fun initializeFetchImageSubscription() { fetchImageSubscription?.unsubscribe() - fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) - .flatMap { - val source = it.second - Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized } - .map { Pair(it, source) } - .concatMap { getMangaDetailsObservable(it.first, it.second) } - .map { Pair(source as CatalogueSource, it) } - - } - .onBackpressureBuffer() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ (source, manga) -> - @Suppress("DEPRECATION") - view?.onMangaInitialized(source, manga) - }, { error -> - Timber.e(error) - }) + fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()).flatMap { + val source = it.second + Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized } + .map { Pair(it, source) } + .concatMap { getMangaDetailsObservable(it.first, it.second) } + .map { Pair(source as CatalogueSource, it) } + }.onBackpressureBuffer().observeOn(AndroidSchedulers.mainThread()) + .subscribe({ (source, manga) -> + @Suppress("DEPRECATION") view?.onMangaInitialized(source, manga) + }, { error -> + Timber.e(error) + }) } /** @@ -222,14 +231,12 @@ open class CatalogueSearchPresenter( * @return an observable of the manga to initialize */ private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable { - return source.fetchMangaDetails(manga) - .flatMap { networkManga -> - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).executeAsBlocking() - Observable.just(manga) - } - .onErrorResumeNext { Observable.just(manga) } + return source.fetchMangaDetails(manga).flatMap { networkManga -> + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + Observable.just(manga) + }.onErrorResumeNext { Observable.just(manga) } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt index 41c133a820..82d1ffd85d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt @@ -1,13 +1,14 @@ package eu.kanade.tachiyomi.ui.catalogue.latest import android.os.Bundle -import androidx.drawerlayout.widget.DrawerLayout import android.view.Menu -import android.view.ViewGroup +import android.view.View import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter +import eu.kanade.tachiyomi.util.view.gone +import kotlinx.android.synthetic.main.catalogue_controller.* /** * Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController]. @@ -18,6 +19,11 @@ class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle putLong(SOURCE_ID_KEY, source.id) }) + override fun onViewCreated(view: View) { + super.onViewCreated(view) + fab.gone() + } + override fun createPresenter(): BrowseCataloguePresenter { return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) } @@ -25,15 +31,5 @@ class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) menu.findItem(R.id.action_search).isVisible = false - menu.findItem(R.id.action_set_filter).isVisible = false - } - - override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? { - return null } - - override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) { - - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt index 2e646638b7..704b375c00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt @@ -10,7 +10,7 @@ import rx.schedulers.Schedulers /** * LatestUpdatesPager inherited from the general Pager. */ -class LatestUpdatesPager(val source: CatalogueSource): Pager() { +class LatestUpdatesPager(val source: CatalogueSource) : Pager() { override fun requestNext(): Observable { return source.fetchLatestUpdates(currentPage) @@ -18,5 +18,4 @@ class LatestUpdatesPager(val source: CatalogueSource): Pager() { .observeOn(AndroidSchedulers.mainThread()) .doOnNext { onPageReceived(it) } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt index a1be55797b..9b514528a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt @@ -12,5 +12,4 @@ class LatestUpdatesPresenter(sourceId: Long) : BrowseCataloguePresenter(sourceId override fun createPager(query: String, filters: FilterList): Pager { return LatestUpdatesPager(source) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt index 0ad0eb90ce..c1ef5d5af6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt @@ -13,38 +13,25 @@ class CategoryAdapter(controller: CategoryController) : /** * Listener called when an item of the list is released. */ - val onItemReleaseListener: OnItemReleaseListener = controller - - /** - * Clears the active selections from the list and the model. - */ - override fun clearSelection() { - super.clearSelection() - (0 until itemCount).forEach { getItem(it)?.isSelected = false } - } + val categoryItemListener: CategoryItemListener = controller /** * Clears the active selections from the model. */ - fun clearModelSelection() { - selectedPositions.forEach { getItem(it)?.isSelected = false } + fun resetEditing(position: Int) { + for (i in 0..itemCount) { + getItem(i)?.isEditing = false + } + getItem(position)?.isEditing = true + notifyDataSetChanged() } - /** - * Toggles the selection of the given position. - * - * @param position The position to toggle. - */ - override fun toggleSelection(position: Int) { - super.toggleSelection(position) - getItem(position)?.isSelected = isSelected(position) - } - - interface OnItemReleaseListener { + interface CategoryItemListener { /** * Called when an item of the list is released. */ fun onItemReleased(position: Int) + fun onCategoryRename(position: Int, newName: String): Boolean + fun onItemDelete(position: Int) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt index 8f610ec033..e5b179c39a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt @@ -1,46 +1,32 @@ package eu.kanade.tachiyomi.ui.category -import com.google.android.material.snackbar.Snackbar -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.view.* +import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.snackbar.BaseTransientBottomBar -import com.jakewharton.rxbinding.view.clicks +import com.google.android.material.snackbar.Snackbar import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.BaseController +import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Companion.CREATE_CATEGORY_ORDER import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.categories_controller.empty_view -import kotlinx.android.synthetic.main.categories_controller.fab -import kotlinx.android.synthetic.main.categories_controller.recycler +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController +import eu.kanade.tachiyomi.util.view.snack +import kotlinx.android.synthetic.main.categories_controller.* /** * Controller to manage the categories for the users' library. */ -class CategoryController : NucleusController(), - ActionMode.Callback, +class CategoryController(bundle: Bundle? = null) : BaseController(bundle), FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - CategoryAdapter.OnItemReleaseListener, + CategoryAdapter.CategoryItemListener, CategoryCreateDialog.Listener, - CategoryRenameDialog.Listener, - UndoHelper.OnActionListener { - - /** - * Object used to show ActionMode toolbar. - */ - private var actionMode: ActionMode? = null + CategoryRenameDialog.Listener { /** * Adapter containing category items. @@ -55,13 +41,13 @@ class CategoryController : NucleusController(), /** * Creates the presenter for this controller. Not to be manually called. */ - override fun createPresenter() = CategoryPresenter() + private val presenter = CategoryPresenter(this) /** * Returns the toolbar title to show when this controller is attached. */ override fun getTitle(): String? { - return resources?.getString(R.string.action_edit_categories) + return resources?.getString(R.string.edit_categories) } /** @@ -81,6 +67,7 @@ class CategoryController : NucleusController(), */ override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForController() adapter = CategoryAdapter(this@CategoryController) recycler.layoutManager = LinearLayoutManager(view.context) @@ -89,20 +76,7 @@ class CategoryController : NucleusController(), adapter?.isHandleDragEnabled = true adapter?.isPermanentDelete = false - fab.clicks().subscribeUntilDestroy { - CategoryCreateDialog(this@CategoryController).showDialog(router, null) - } - - val fabBaseMarginBottom = fab?.marginBottom ?: 0 - recycler.doOnApplyWindowInsets { v, insets, padding -> - - fab?.updateLayoutParams { - bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom - } - // offset the recycler by the fab's inset + some inset on top - v.updatePaddingRelative(bottom = padding.bottom + (fab?.marginBottom ?: 0) + - fabBaseMarginBottom + (fab?.height ?: 0)) - } + presenter.getCategories() } /** @@ -113,123 +87,26 @@ class CategoryController : NucleusController(), override fun onDestroyView(view: View) { // Manually call callback to delete categories if required snack?.dismiss() + view.clearFocus() confirmDelete() snack = null - actionMode = null adapter = null super.onDestroyView(view) } + override fun handleBack(): Boolean { + view?.clearFocus() + confirmDelete() + return super.handleBack() + } + /** * Called from the presenter when the categories are updated. * * @param categories The new list of categories to display. */ fun setCategories(categories: List) { - actionMode?.finish() adapter?.updateDataSet(categories) - if (categories.isNotEmpty()) { - empty_view.hide() - val selected = categories.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(categories.indexOf(it)) } - } - } else { - empty_view.show(R.drawable.ic_shape_black_128dp, R.string.information_empty_category) - } - } - - /** - * Called when action mode is first created. The menu supplied will be used to generate action - * buttons for the action mode. - * - * @param mode ActionMode being created. - * @param menu Menu used to populate action buttons. - * @return true if the action mode should be created, false if entering this mode should be - * aborted. - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate menu. - mode.menuInflater.inflate(R.menu.category_selection, menu) - // Enable adapter multi selection. - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - /** - * Called to refresh an action mode's action menu whenever it is invalidated. - * - * @param mode ActionMode being prepared. - * @param menu Menu used to populate action buttons. - * @return true if the menu or action mode was updated, false otherwise. - */ - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val adapter = adapter ?: return false - val count = adapter.selectedItemCount - mode.title = resources?.getString(R.string.label_selected, count) - - // Show edit button only when one item is selected - val editItem = mode.menu.findItem(R.id.action_edit) - editItem.isVisible = count == 1 - return true - } - - /** - * Called to report a user click on an action button. - * - * @param mode The current ActionMode. - * @param item The item that was clicked. - * @return true if this callback handled the event, false if the standard MenuItem invocation - * should continue. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - val adapter = adapter ?: return false - - when (item.itemId) { - R.id.action_delete -> { - adapter.removeItems(adapter.selectedPositions) - snack = - view?.snack(R.string.snack_categories_deleted, Snackbar.LENGTH_INDEFINITE) { - var undoing = false - setAction(R.string.action_undo) { - adapter.restoreDeletedItems() - undoing = true - } - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (!undoing) confirmDelete() - } - }) - } - (activity as? MainActivity)?.setUndoSnackBar(snack) - mode.finish() - } - R.id.action_edit -> { - // Edit selected category - if (adapter.selectedItemCount == 1) { - val position = adapter.selectedPositions.first() - val category = adapter.getItem(position)?.category - if (category != null) { - editCategory(category) - } - } - } - else -> return false - } - return true - } - - /** - * Called when an action mode is about to be exited and destroyed. - * - * @param mode The current ActionMode being destroyed. - */ - override fun onDestroyActionMode(mode: ActionMode) { - // Reset adapter to single selection - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null } /** @@ -239,50 +116,45 @@ class CategoryController : NucleusController(), * @return true if this click should enable selection mode. */ override fun onItemClick(view: View?, position: Int): Boolean { - // Check if action mode is initialized and selected item exist. - return if (actionMode != null && position != RecyclerView.NO_POSITION) { - toggleSelection(position) - true - } else { - false - } + adapter?.resetEditing(position) + return true } - /** - * Called when an item in the list is long clicked. - * - * @param position The position of the clicked item. - */ - override fun onItemLongClick(position: Int) { - val activity = activity as? AppCompatActivity ?: return - - // Check if action mode is initialized. - if (actionMode == null) { - // Initialize action mode - actionMode = activity.startSupportActionMode(this) - } - - // Set item as selected - toggleSelection(position) + override fun onCategoryRename(position: Int, newName: String): Boolean { + val category = adapter?.getItem(position)?.category ?: return false + if (category.order == CREATE_CATEGORY_ORDER) + return (presenter.createCategory(newName)) + return (presenter.renameCategory(category, newName)) } - /** - * Toggle the selection state of an item. - * If the item was the last one in the selection and is unselected, the ActionMode is finished. - * - * @param position The position of the item to toggle. - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - - //Mark the position selected - adapter.toggleSelection(position) - - if (adapter.selectedItemCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - } + override fun onItemDelete(position: Int) { + MaterialDialog(activity!!) + .title(R.string.confirm_category_deletion) + .message(R.string.confirm_category_deletion_message) + .positiveButton(R.string.delete) { + deleteCategory(position) + } + .negativeButton(android.R.string.no) + .show() + } + + private fun deleteCategory(position: Int) { + adapter?.removeItem(position) + snack = + view?.snack(R.string.category_deleted, Snackbar.LENGTH_INDEFINITE) { + var undoing = false + setAction(R.string.undo) { + adapter?.restoreDeletedItems() + undoing = true + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!undoing) confirmDelete() + } + }) + } + (activity as? MainActivity)?.setUndoSnackBar(snack) } /** @@ -296,31 +168,10 @@ class CategoryController : NucleusController(), presenter.reorderCategories(categories) } - /** - * Called when the undo action is clicked in the snackbar. - * - * @param action The action performed. - */ - override fun onActionCanceled(action: Int, positions: MutableList?) { - adapter?.restoreDeletedItems() - snack = null - } - - /** - * Called when the time to restore the items expires. - * - * @param action The action performed. - * @param event The event that triggered the action - */ - override fun onActionConfirmed(action: Int, event: Int) { - val adapter = adapter ?: return - presenter.deleteCategories(adapter.deletedItems.map { it.category }) - snack = null - } - fun confirmDelete() { val adapter = adapter ?: return - presenter.deleteCategories(adapter.deletedItems.map { it.category }) + presenter.deleteCategory(adapter.deletedItems.map { it.category }.firstOrNull()) + adapter.confirmDeletion() snack = null } @@ -356,7 +207,6 @@ class CategoryController : NucleusController(), * Called from the presenter when a category with the given name already exists. */ fun onCategoryExistsError() { - activity?.toast(R.string.error_category_exists) + activity?.toast(R.string.category_with_name_exists) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt index c6d0cc37a7..1c4681f87a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt @@ -26,7 +26,7 @@ class CategoryCreateDialog(bundle: Bundle? = null) : DialogController(bundle) */ override fun onCreateDialog(savedViewState: Bundle?): Dialog { return MaterialDialog(activity!!) - .title(R.string.action_add_category) + .title(R.string.add_category) .positiveButton(android.R.string.ok) .negativeButton(android.R.string.cancel) .input(hintRes = R.string.name) { _, input -> @@ -37,5 +37,4 @@ class CategoryCreateDialog(bundle: Bundle? = null) : DialogController(bundle) interface Listener { fun createCategory(name: String) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt index 740a141c0a..166bc5d96f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt @@ -1,12 +1,21 @@ package eu.kanade.tachiyomi.ui.category +import android.content.Context +import android.graphics.drawable.Drawable +import android.text.InputType import android.view.View +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.core.content.ContextCompat +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.util.view.getRound -import kotlinx.android.synthetic.main.categories_item.image -import kotlinx.android.synthetic.main.categories_item.reorder -import kotlinx.android.synthetic.main.categories_item.title +import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Companion.CREATE_CATEGORY_ORDER +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.android.synthetic.main.categories_item.* /** * Holder used to display category items. @@ -17,15 +26,14 @@ import kotlinx.android.synthetic.main.categories_item.title class CategoryHolder(view: View, val adapter: CategoryAdapter) : BaseFlexibleViewHolder(view, adapter) { init { - // Create round letter image onclick to simulate long click - image.setOnClickListener { - // Simulate long click on this view to enter selection mode - onLongClick(view) + edit_button.setOnClickListener { + submitChanges() } - - setDragHandleView(reorder) } + var createCategory = false + private var regularDrawable: Drawable? = null + /** * Binds this holder with the given category. * @@ -34,13 +42,87 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : BaseFlexibleVie fun bind(category: Category) { // Set capitalized title. title.text = category.name.capitalize() + edit_text.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submitChanges() + } + true + } + createCategory = category.order == CREATE_CATEGORY_ORDER + if (createCategory) { + title.setTextColor(ContextCompat.getColor(itemView.context, R.color.textColorHint)) + regularDrawable = ContextCompat.getDrawable(itemView.context, R.drawable + .ic_add_white_24dp) + image.gone() + edit_button.setImageDrawable(null) + edit_text.setText("") + edit_text.hint = title.text + } else { + title.setTextColor(ContextCompat.getColor(itemView.context, R.color.textColorPrimary)) + regularDrawable = ContextCompat.getDrawable(itemView.context, R.drawable + .ic_drag_handle_black_24dp) + image.visible() + edit_text.setText(title.text) + } + } - // Update circle letter image. - itemView.post { - image.setImageDrawable(image.getRound(category.name.take(1).toUpperCase(),false)) + fun isEditing(editing: Boolean) { + itemView.isActivated = editing + title.visibility = if (editing) View.INVISIBLE else View.VISIBLE + edit_text.visibility = if (!editing) View.INVISIBLE else View.VISIBLE + if (editing) { + edit_text.inputType = InputType.TYPE_TEXT_FLAG_AUTO_CORRECT + edit_text.requestFocus() + edit_text.selectAll() + edit_button.setImageDrawable(ContextCompat.getDrawable(itemView.context, R.drawable.ic_check_white_24dp)) + edit_button.drawable.mutate().setTint(itemView.context.getResourceColor(R.attr.colorAccent)) + showKeyboard() + if (!createCategory) { + reorder.setImageDrawable( + ContextCompat.getDrawable( + itemView.context, R.drawable.ic_delete_white_24dp + ) + ) + reorder.setOnClickListener { + adapter.categoryItemListener.onItemDelete(adapterPosition) + } + } + } else { + if (!createCategory) { + setDragHandleView(reorder) + edit_button.setImageDrawable(ContextCompat.getDrawable(itemView.context, R.drawable.ic_edit_white_24dp)) + } else { + edit_button.setImageDrawable(null) + reorder.setOnTouchListener { _, _ -> true } + } + edit_text.clearFocus() + edit_button.drawable?.mutate()?.setTint(ContextCompat.getColor(itemView.context, R + .color.gray_button)) + reorder.setImageDrawable(regularDrawable) } } + private fun submitChanges() { + if (edit_text.visibility == View.VISIBLE) { + if (adapter.categoryItemListener + .onCategoryRename(adapterPosition, edit_text.text.toString())) { + isEditing(false) + edit_text.inputType = InputType.TYPE_NULL + if (!createCategory) + title.text = edit_text.text.toString() + } + } else { + itemView.performClick() + } + } + + private fun showKeyboard() { + val inputMethodManager: InputMethodManager = + itemView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(edit_text, WindowManager.LayoutParams + .SOFT_INPUT_ADJUST_PAN) + } + /** * Called when an item is released. * @@ -48,7 +130,6 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : BaseFlexibleVie */ override fun onItemReleased(position: Int) { super.onItemReleased(position) - adapter.onItemReleaseListener.onItemReleased(position) + adapter.categoryItemListener.onItemReleased(position) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt index 9ea353f92b..f445e71f11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt @@ -7,6 +7,7 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Companion.CREATE_CATEGORY_ORDER /** * Category item for a recycler view. @@ -16,7 +17,7 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem>, holder: CategoryHolder, position: Int, payloads: MutableList) { holder.bind(category) + holder.isEditing(isEditing) } /** * Returns true if this item is draggable. */ override fun isDraggable(): Boolean { - return true + return category.order != CREATE_CATEGORY_ORDER && !isEditing } override fun equals(other: Any?): Boolean { @@ -65,5 +67,4 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem() { + private val controller: CategoryController, + private val db: DatabaseHelper = Injekt.get(), + preferences: PreferencesHelper = Injekt.get() +) { + + private val context = preferences.context /** * List containing categories. */ - private var categories: List = emptyList() + private var categories: MutableList = mutableListOf() /** * Called when the presenter is created. - * - * @param savedState The saved state of this presenter. */ - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - db.getCategories().asRxObservable() - .doOnNext { categories = it } - .map { it.map(::CategoryItem) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(CategoryController::setCategories) + fun getCategories() { + if (categories.isNotEmpty()) { + controller.setCategories(categories.map(::CategoryItem)) + } + GlobalScope.launch(Dispatchers.IO) { + categories.clear() + categories.add(newCategory()) + categories.addAll(db.getCategories().executeAsBlocking()) + val catItems = categories.map(::CategoryItem) + withContext(Dispatchers.Main) { + controller.setCategories(catItems) + } + } + } + + private fun newCategory(): Category { + val default = Category.create(context.getString(R.string.create_new_category)) + default.order = CREATE_CATEGORY_ORDER + default.id = Int.MIN_VALUE + return default } /** @@ -41,11 +57,11 @@ class CategoryPresenter( * * @param name The name of the category to create. */ - fun createCategory(name: String) { + fun createCategory(name: String): Boolean { // Do not allow duplicate categories. - if (categoryExists(name)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() }) - return + if (categoryExists(name, null)) { + controller.onCategoryExistsError() + return false } // Create category. @@ -55,16 +71,26 @@ class CategoryPresenter( cat.order = categories.map { it.order + 1 }.max() ?: 0 // Insert into database. - db.insertCategory(cat).asRxObservable().subscribe() + + cat.mangaSort = 'a' + db.insertCategory(cat).executeAsBlocking() + val cats = db.getCategories().executeAsBlocking() + val newCat = cats.find { it.name == name } ?: return false + categories.add(1, newCat) + reorderCategories(categories) + return true } /** * Deletes the given categories from the database. * - * @param categories The list of categories to delete. + * @param category The category to delete. */ - fun deleteCategories(categories: List) { - db.deleteCategories(categories).asRxObservable().subscribe() + fun deleteCategory(category: Category?) { + val safeCategory = category ?: return + db.deleteCategory(safeCategory).executeAsBlocking() + categories.remove(safeCategory) + controller.setCategories(categories.map(::CategoryItem)) } /** @@ -74,10 +100,12 @@ class CategoryPresenter( */ fun reorderCategories(categories: List) { categories.forEachIndexed { i, category -> - category.order = i + if (category.order != CREATE_CATEGORY_ORDER) + category.order = i - 1 } - - db.insertCategories(categories).asRxObservable().subscribe() + db.insertCategories(categories.filter { it.order != CREATE_CATEGORY_ORDER }).executeAsBlocking() + this.categories = categories.sortedBy { it.order }.toMutableList() + controller.setCategories(categories.map(::CategoryItem)) } /** @@ -86,22 +114,28 @@ class CategoryPresenter( * @param category The category to rename. * @param name The new name of the category. */ - fun renameCategory(category: Category, name: String) { + fun renameCategory(category: Category, name: String): Boolean { // Do not allow duplicate categories. - if (categoryExists(name)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() }) - return + if (categoryExists(name, category.id)) { + controller.onCategoryExistsError() + return false } category.name = name - db.insertCategory(category).asRxObservable().subscribe() + db.insertCategory(category).executeAsBlocking() + categories.find { it.id == category.id }?.name = name + controller.setCategories(categories.map(::CategoryItem)) + return true } /** * Returns true if a category with the given name already exists. */ - fun categoryExists(name: String): Boolean { - return categories.any { it.name.equals(name, true) } + private fun categoryExists(name: String, id: Int?): Boolean { + return categories.any { it.name.equals(name, true) && id != it.id } } -} \ No newline at end of file + companion object { + const val CREATE_CATEGORY_ORDER = -2 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt index 5ce4d6f6f8..8d0940e3bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt @@ -36,7 +36,7 @@ class CategoryRenameDialog(bundle: Bundle? = null) : DialogController(bundle) */ override fun onCreateDialog(savedViewState: Bundle?): Dialog { return MaterialDialog(activity!!) - .title(R.string.action_rename_category) + .title(R.string.rename_category) .negativeButton(android.R.string.cancel) .input(hintRes = R.string.name, prefill = currentName) { _, input -> currentName = input.toString() @@ -81,5 +81,4 @@ class CategoryRenameDialog(bundle: Bundle? = null) : DialogController(bundle) private companion object { const val CATEGORY_KEY = "CategoryRenameDialog.category" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt index 458ca153c7..f8a7cc32bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt @@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter * * @param context the context of the fragment containing this adapter. */ -class DownloadAdapter(controller: DownloadController) : FlexibleAdapter(null, controller, +class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter(null, controller, true) { /** @@ -21,6 +21,12 @@ class DownloadAdapter(controller: DownloadController) : FlexibleAdapter() + + private var scope = CoroutineScope(Job() + Dispatchers.Default) + + /** + * Property to get the queue from the download manager. + */ + val downloadQueue: DownloadQueue + get() = downloadManager.queue + + fun getItems() { + scope.launch { + val items = downloadQueue.map(::DownloadItem) + val hasChanged = if (this@DownloadBottomPresenter.items.size != items.size) true + else { + val oldItemsIds = this@DownloadBottomPresenter.items.mapNotNull { + it.download.chapter.id + }.toLongArray() + val newItemsIds = items.mapNotNull { it.download.chapter.id }.toLongArray() + !oldItemsIds.contentEquals(newItemsIds) + } + this@DownloadBottomPresenter.items = items + if (hasChanged) { + withContext(Dispatchers.Main) { sheet.onNextDownloads(items) } + } + } + } + + /** + * Pauses the download queue. + */ + fun pauseDownloads() { + downloadManager.pauseDownloads() + } + + /** + * Clears the download queue. + */ + fun clearQueue() { + downloadManager.clearQueue() + } + + fun reorder(downloads: List) { + downloadManager.reorderQueue(downloads) + } + + fun cancelDownload(download: Download) { + downloadManager.deletePendingDownloads(download) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt new file mode 100644 index 0000000000..c0a0d623a0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt @@ -0,0 +1,271 @@ +package eu.kanade.tachiyomi.ui.download + +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import android.view.Menu +import android.view.MenuItem +import android.widget.LinearLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.extension.ExtensionDividerItemDecoration +import eu.kanade.tachiyomi.ui.recents.RecentsController +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import kotlinx.android.synthetic.main.download_bottom_sheet.view.* + +class DownloadBottomSheet @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = + null +) : LinearLayout(context, attrs), + DownloadAdapter.DownloadItemListener { + lateinit var controller: RecentsController + var sheetBehavior: BottomSheetBehavior<*>? = null + + /** + * Adapter containing the active downloads. + */ + private var adapter: DownloadAdapter? = null + + private val presenter = DownloadBottomPresenter(this) + + /** + * Whether the download queue is running or not. + */ + private var isRunning: Boolean = false + private var activity: Activity? = null + + fun onCreate(controller: RecentsController) { + // Initialize adapter, scroll listener and recycler views + adapter = DownloadAdapter(this) + sheetBehavior = BottomSheetBehavior.from(this) + activity = controller.activity + // Create recycler and set adapter. + dl_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + dl_recycler.adapter = adapter + adapter?.isHandleDragEnabled = true + adapter?.isSwipeEnabled = true + dl_recycler.setHasFixedSize(true) + dl_recycler.addItemDecoration(ExtensionDividerItemDecoration(context)) + dl_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + this.controller = controller + updateDLTitle() + + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = context.obtainStyledAttributes(attrsArray) + val headerHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + dl_recycler.doOnApplyWindowInsets { _, windowInsets, _ -> + dl_recycler.updateLayoutParams { + topMargin = windowInsets.systemWindowInsetTop + headerHeight - sheet_layout.height + } + } + sheet_layout.setOnClickListener { + if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } else { + sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + update() + setInformationView() + if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED && sheetBehavior?.isHideable == true) sheetBehavior?.state = + BottomSheetBehavior.STATE_HIDDEN + } + + fun update() { + presenter.getItems() + onQueueStatusChange(!presenter.downloadManager.isPaused()) + } + + private fun updateDLTitle() { + val extCount = presenter.downloadQueue.firstOrNull() + title_text.text = if (extCount != null) resources.getString( + R.string.downloading_, extCount.chapter.name + ) + else "" + } + + /** + * Called when the queue's status has changed. Updates the visibility of the buttons. + * + * @param running whether the queue is now running or not. + */ + private fun onQueueStatusChange(running: Boolean) { + val oldRunning = isRunning + isRunning = running + if (oldRunning != running) { + activity?.invalidateOptionsMenu() + + // Check if download queue is empty and update information accordingly. + setInformationView() + } + } + + /** + * Called from the presenter to assign the downloads for the adapter. + * + * @param downloads the downloads from the queue. + */ + fun onNextDownloads(downloads: List) { + activity?.invalidateOptionsMenu() + setInformationView() + adapter?.updateDataSet(downloads) + setBottomSheet() + } + + /** + * Called when the progress of a download changes. + * + * @param download the download whose progress has changed. + */ + fun onUpdateProgress(download: Download) { + getHolder(download)?.notifyProgress() + } + + /** + * Called when a page of a download is downloaded. + * + * @param download the download whose page has been downloaded. + */ + fun onUpdateDownloadedPages(download: Download) { + getHolder(download)?.notifyDownloadedPages() + } + + /** + * Returns the holder for the given download. + * + * @param download the download to find. + * @return the holder of the download or null if it's not bound. + */ + private fun getHolder(download: Download): DownloadHolder? { + return dl_recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder + } + + /** + * Set information view when queue is empty + */ + private fun setInformationView() { + updateDLTitle() + setBottomSheet() + if (presenter.downloadQueue.isEmpty()) { + empty_view?.show( + R.drawable.ic_file_download_black_128dp, + R.string.nothing_is_downloading) + } else { + empty_view?.hide() + } + } + + fun prepareMenu(menu: Menu) { + // Set start button visibility. + menu.findItem(R.id.start_queue)?.isVisible = !isRunning && !presenter.downloadQueue.isEmpty() + + // Set pause button visibility. + menu.findItem(R.id.pause_queue)?.isVisible = isRunning && !presenter.downloadQueue.isEmpty() + + // Set clear button visibility. + menu.findItem(R.id.clear_queue)?.isVisible = !presenter.downloadQueue.isEmpty() + + // Set reorder button visibility. + menu.findItem(R.id.reorder)?.isVisible = !presenter.downloadQueue.isEmpty() + } + + fun onOptionsItemSelected(item: MenuItem): Boolean { + val context = activity ?: return false + when (item.itemId) { + R.id.start_queue -> DownloadService.start(context) + R.id.pause_queue -> { + DownloadService.stop(context) + presenter.pauseDownloads() + } + R.id.clear_queue -> { + DownloadService.stop(context) + presenter.clearQueue() + } + R.id.newest, R.id.oldest -> { + val adapter = adapter ?: return false + val items = adapter.currentItems.sortedBy { it.download.chapter.date_upload } + .toMutableList() + if (item.itemId == R.id.newest) + items.reverse() + adapter.updateDataSet(items) + val downloads = items.mapNotNull { it.download } + presenter.reorder(downloads) + } + } + return true + } + + fun dismiss() { + if (sheetBehavior?.isHideable == true) { + sheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN + } else { + sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + private fun setBottomSheet() { + val hasQueue = presenter.downloadQueue.isNotEmpty() + if (hasQueue) { + sheetBehavior?.skipCollapsed = !hasQueue + if (sheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN) sheetBehavior?.state = + BottomSheetBehavior.STATE_COLLAPSED + sheetBehavior?.isHideable = !hasQueue + } else { + sheetBehavior?.isHideable = !hasQueue + sheetBehavior?.skipCollapsed = !hasQueue + if (sheetBehavior?.state == BottomSheetBehavior.STATE_COLLAPSED) sheetBehavior?.state = + BottomSheetBehavior.STATE_HIDDEN + } + controller.setPadding(!hasQueue) + } + + /** + * Called when an item is released from a drag. + * + * @param position The position of the released item. + */ + override fun onItemReleased(position: Int) { + val adapter = adapter ?: return + val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } + presenter.reorder(downloads) + } + + override fun onItemRemoved(position: Int) { + val download = adapter?.getItem(position)?.download ?: return + presenter.cancelDownload(download) + + adapter?.removeItem(position) + val adapter = adapter ?: return + val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } + presenter.reorder(downloads) + } + + /** + * Called when the menu item of a download is pressed + * + * @param position The position of the item + * @param menuItem The menu Item pressed + */ + override fun onMenuItemClick(position: Int, menuItem: MenuItem) { + when (menuItem.itemId) { + R.id.move_to_top, R.id.move_to_bottom -> { + val items = adapter?.currentItems?.toMutableList() ?: return + val item = items[position] + items.remove(item) + if (menuItem.itemId == R.id.move_to_top) + items.add(0, item) + else + items.add(item) + adapter?.updateDataSet(items) + val downloads = items.mapNotNull { it.download } + presenter.reorder(downloads) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt new file mode 100644 index 0000000000..eda22a6105 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt @@ -0,0 +1,110 @@ +package eu.kanade.tachiyomi.ui.download + +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.android.synthetic.main.download_button.view.* + +class DownloadButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs) { + + private val activeColor = context.getResourceColor(R.attr.colorAccent) + private val progressBGColor = ContextCompat.getColor(context, + R.color.divider) + private val disabledColor = ContextCompat.getColor(context, + R.color.material_on_surface_disabled) + private val primaryColor = context.getResourceColor(android.R.attr.textColorPrimaryInverse) + private val downloadedColor = ContextCompat.getColor(context, + R.color.download) + private val errorColor = ContextCompat.getColor(context, + R.color.red_error) + private val filledCircle = ContextCompat.getDrawable(context, + R.drawable.filled_circle)?.mutate() + private val borderCircle = ContextCompat.getDrawable(context, + R.drawable.border_circle)?.mutate() + private val downloadDrawable = ContextCompat.getDrawable(context, + R.drawable.ic_arrow_down_white_24dp)?.mutate() + private val checkDrawable = ContextCompat.getDrawable(context, + R.drawable.ic_check_white_24dp)?.mutate() + private var isAnimating = false + private var iconAnimation: ObjectAnimator? = null + + fun setDownloadStatus(state: Int, progress: Int = 0) { + if (state != Download.DOWNLOADING) { + iconAnimation?.cancel() + download_icon.alpha = 1f + isAnimating = false + } + download_icon.setImageDrawable(if (state == Download.CHECKED) + checkDrawable else downloadDrawable) + when (state) { + Download.CHECKED -> { + download_progress.gone() + download_border.visible() + download_progress_indeterminate.gone() + download_border.setImageDrawable(filledCircle) + download_border.drawable.setTint(activeColor) + download_icon.drawable.setTint(Color.WHITE) + } + Download.NOT_DOWNLOADED -> { + download_border.visible() + download_progress.gone() + download_progress_indeterminate.gone() + download_border.setImageDrawable(borderCircle) + download_border.drawable.setTint(activeColor) + download_icon.drawable.setTint(activeColor) + } + Download.QUEUE -> { + download_border.gone() + download_progress.gone() + download_progress_indeterminate.visible() + download_progress.isIndeterminate = true + download_icon.drawable.setTint(disabledColor) + } + Download.DOWNLOADING -> { + download_border.visible() + download_progress.visible() + download_progress_indeterminate.gone() + download_border.setImageDrawable(borderCircle) + download_progress.isIndeterminate = false + download_progress.progress = progress + download_border.drawable.setTint(progressBGColor) + download_progress.progressDrawable?.setTint(downloadedColor) + download_icon.drawable.setTint(disabledColor) + if (!isAnimating) { + iconAnimation = ObjectAnimator.ofFloat(download_icon, "alpha", 1f, 0f).apply { + duration = 1000 + repeatCount = ObjectAnimator.INFINITE + repeatMode = ObjectAnimator.REVERSE + } + iconAnimation?.start() + isAnimating = true + } + } + Download.DOWNLOADED -> { + download_progress.gone() + download_border.visible() + download_progress_indeterminate.gone() + download_border.setImageDrawable(filledCircle) + download_border.drawable.setTint(downloadedColor) + download_icon.drawable.setTint(primaryColor) + } + Download.ERROR -> { + download_progress.gone() + download_border.visible() + download_progress_indeterminate.gone() + download_border.setImageDrawable(borderCircle) + download_border.drawable.setTint(errorColor) + download_icon.drawable.setTint(errorColor) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt deleted file mode 100644 index 90767d82ba..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt +++ /dev/null @@ -1,315 +0,0 @@ -package eu.kanade.tachiyomi.ui.download - -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.DownloadService -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener -import kotlinx.android.synthetic.main.download_controller.* -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.injectLazy -import java.util.HashMap -import java.util.concurrent.TimeUnit - -/** - * Controller that shows the currently active downloads. - * Uses R.layout.fragment_download_queue. - */ -class DownloadController : NucleusController(), - DownloadAdapter.DownloadItemListener { - - /** - * Adapter containing the active downloads. - */ - private var adapter: DownloadAdapter? = null - - /** - * Map of subscriptions for active downloads. - */ - private val progressSubscriptions by lazy { HashMap() } - - /** - * Whether the download queue is running or not. - */ - private var isRunning: Boolean = false - - init { - setHasOptionsMenu(true) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.download_controller, container, false) - } - - override fun createPresenter(): DownloadPresenter { - return DownloadPresenter() - } - - override fun getTitle(): String? { - return resources?.getString(R.string.label_download_queue) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Check if download queue is empty and update information accordingly. - setInformationView() - - // Initialize adapter. - adapter = DownloadAdapter(this@DownloadController) - recycler.adapter = adapter - adapter?.isHandleDragEnabled = true - - // Set the layout manager for the recycler and fixed size. - recycler.layoutManager = LinearLayoutManager(view.context) - recycler.setHasFixedSize(true) - recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - - // Suscribe to changes - DownloadService.runningRelay - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onQueueStatusChange(it) } - - presenter.getDownloadStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onStatusChange(it) } - - presenter.getDownloadProgressObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onUpdateDownloadedPages(it) } - } - - override fun onDestroyView(view: View) { - for (subscription in progressSubscriptions.values) { - subscription.unsubscribe() - } - progressSubscriptions.clear() - adapter = null - super.onDestroyView(view) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.download_queue, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - // Set start button visibility. - menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() - - // Set pause button visibility. - menu.findItem(R.id.pause_queue).isVisible = isRunning - - // Set clear button visibility. - menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() - - // Set reorder button visibility. - menu.findItem(R.id.reorder).isVisible = !presenter.downloadQueue.isEmpty() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val context = applicationContext ?: return false - when (item.itemId) { - R.id.start_queue -> DownloadService.start(context) - R.id.pause_queue -> { - DownloadService.stop(context) - presenter.pauseDownloads() - } - R.id.clear_queue -> { - DownloadService.stop(context) - presenter.clearQueue() - } - R.id.newest, R.id.oldest -> { - val adapter = adapter ?: return false - val items = adapter.currentItems.sortedBy { it.download.chapter.date_upload } - .toMutableList() - if (item.itemId == R.id.newest) - items.reverse() - adapter.updateDataSet(items) - val downloads = items.mapNotNull { it.download } - presenter.reorder(downloads) - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Called when the status of a download changes. - * - * @param download the download whose status has changed. - */ - private fun onStatusChange(download: Download) { - when (download.status) { - Download.DOWNLOADING -> { - observeProgress(download) - // Initial update of the downloaded pages - onUpdateDownloadedPages(download) - } - Download.DOWNLOADED -> { - unsubscribeProgress(download) - onUpdateProgress(download) - onUpdateDownloadedPages(download) - } - Download.ERROR -> unsubscribeProgress(download) - } - } - - /** - * Observe the progress of a download and notify the view. - * - * @param download the download to observe its progress. - */ - private fun observeProgress(download: Download) { - val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) - // Get the sum of percentages for all the pages. - .flatMap { - Observable.from(download.pages) - .map(Page::progress) - .reduce { x, y -> x + y } - } - // Keep only the latest emission to avoid backpressure. - .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { progress -> - // Update the view only if the progress has changed. - if (download.totalProgress != progress) { - download.totalProgress = progress - onUpdateProgress(download) - } - } - - // Avoid leaking subscriptions - progressSubscriptions.remove(download)?.unsubscribe() - - progressSubscriptions[download] = subscription - } - - /** - * Unsubscribes the given download from the progress subscriptions. - * - * @param download the download to unsubscribe. - */ - private fun unsubscribeProgress(download: Download) { - progressSubscriptions.remove(download)?.unsubscribe() - } - - /** - * Called when the queue's status has changed. Updates the visibility of the buttons. - * - * @param running whether the queue is now running or not. - */ - private fun onQueueStatusChange(running: Boolean) { - isRunning = running - activity?.invalidateOptionsMenu() - - // Check if download queue is empty and update information accordingly. - setInformationView() - } - - /** - * Called from the presenter to assign the downloads for the adapter. - * - * @param downloads the downloads from the queue. - */ - fun onNextDownloads(downloads: List) { - activity?.invalidateOptionsMenu() - setInformationView() - adapter?.updateDataSet(downloads) - } - - /** - * Called when the progress of a download changes. - * - * @param download the download whose progress has changed. - */ - fun onUpdateProgress(download: Download) { - getHolder(download)?.notifyProgress() - } - - /** - * Called when a page of a download is downloaded. - * - * @param download the download whose page has been downloaded. - */ - fun onUpdateDownloadedPages(download: Download) { - getHolder(download)?.notifyDownloadedPages() - } - - /** - * Returns the holder for the given download. - * - * @param download the download to find. - * @return the holder of the download or null if it's not bound. - */ - private fun getHolder(download: Download): DownloadHolder? { - return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder - } - - /** - * Set information view when queue is empty - */ - private fun setInformationView() { - if (presenter.downloadQueue.isEmpty()) { - empty_view?.show(R.drawable.ic_file_download_black_128dp, - R.string.information_no_downloads) - } else { - empty_view?.hide() - } - } - - /** - * Called when an item is released from a drag. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - val adapter = adapter ?: return - val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } - presenter.reorder(downloads) - } - - /** - * Called when the menu item of a download is pressed - * - * @param position The position of the item - * @param menuItem The menu Item pressed - */ - override fun onMenuItemClick(position: Int, menuItem: MenuItem) { - when (menuItem.itemId) { - R.id.move_to_top, R.id.move_to_bottom -> { - val items = adapter?.currentItems?.toMutableList() ?: return - val item = items[position] - items.remove(item) - if (menuItem.itemId == R.id.move_to_top) - items.add(0, item) - else - items.add(item) - adapter?.updateDataSet(items) - val downloads = items.mapNotNull { it.download } - presenter.reorder(downloads) - } - R.id.cancel_download -> { - val download = adapter?.getItem(position)?.download ?: return - presenter.cancelDownload(download) - - adapter?.removeItem(position) - val adapter = adapter ?: return - val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } - presenter.reorder(downloads) - } - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt index d62fd0a8cb..e3c9c13d92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt @@ -1,14 +1,14 @@ package eu.kanade.tachiyomi.ui.download import android.view.View -import android.widget.PopupMenu +import androidx.appcompat.widget.PopupMenu import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setVectorCompat +import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.download_item.* -import kotlinx.android.synthetic.main.download_item.migration_menu /** * Class used to hold the data of a download. @@ -38,7 +38,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : chapter_title.text = download.chapter.name // Update the manga title - manga_title.text = download.manga.currentTitle() + manga_full_title.text = download.manga.title // Update the progress bar and the number of downloaded pages val pages = download.pages @@ -52,12 +52,12 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : notifyDownloadedPages() } + migration_menu.visibleIf(adapterPosition != 0 || adapterPosition != adapter.itemCount - 1) migration_menu.setVectorCompat( R.drawable.ic_more_vert_black_24dp, view.context .getResourceColor(R.attr.icon_color)) } - /** * Updates the progress bar of the download. */ @@ -66,7 +66,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : if (download_progress.max == 1) { download_progress.max = pages.size * 100 } - download_progress.progress = download.totalProgress + download_progress.progress = download.pageProgress } /** @@ -82,7 +82,6 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : adapter.downloadItemListener.onItemReleased(position) } - private fun showPopupMenu(view: View) { val item = adapter.getItem(adapterPosition) ?: return @@ -98,7 +97,6 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : popup.menu.findItem(R.id.move_to_bottom).isVisible = adapterPosition != adapter .itemCount - 1 - // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> adapter.downloadItemListener.onMenuItemClick(adapterPosition, menuItem) @@ -109,4 +107,15 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : popup.show() } + override fun getFrontView(): View { + return front_view + } + + override fun getRearRightView(): View { + return right_view + } + + override fun getRearLeftView(): View { + return left_view + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt index 5627ea127f..b8fef48d1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt @@ -28,8 +28,11 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem>): DownloadHolder { + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): DownloadHolder { return DownloadHolder(view, adapter as DownloadAdapter) } @@ -41,8 +44,12 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem>, - holder: DownloadHolder, position: Int, payloads: MutableList) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: DownloadHolder, + position: Int, + payloads: MutableList + ) { holder.bind(download) } @@ -64,5 +71,4 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem() { - - /** - * Download manager. - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Property to get the queue from the download manager. - */ - val downloadQueue: DownloadQueue - get() = downloadManager.queue - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - downloadQueue.getUpdatedObservable() - .observeOn(AndroidSchedulers.mainThread()) - .map { it.map(::DownloadItem) } - .subscribeLatestCache(DownloadController::onNextDownloads) { _, error -> - Timber.e(error) - } - } - - fun getDownloadStatusObservable(): Observable { - return downloadQueue.getStatusObservable() - .startWith(downloadQueue.getActiveDownloads()) - } - - fun getDownloadProgressObservable(): Observable { - return downloadQueue.getProgressObservable() - .onBackpressureBuffer() - } - - /** - * Pauses the download queue. - */ - fun pauseDownloads() { - downloadManager.pauseDownloads() - } - - /** - * Clears the download queue. - */ - fun clearQueue() { - downloadManager.clearQueue() - } - - fun reorder(downloads: List) { - downloadManager.reorderQueue(downloads) - } - - fun cancelDownload(download: Download) { - downloadManager.deletePendingDownloads(download) - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt index f8b1f56715..d40c17c7e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt @@ -3,17 +3,19 @@ package eu.kanade.tachiyomi.ui.extension import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener import eu.kanade.tachiyomi.util.system.getResourceColor /** * Adapter that holds the catalogue cards. * - * @param controller instance of [ExtensionController]. + * @param listener instance of [OnButtonClickListener]. */ -class ExtensionAdapter(val controller: ExtensionController) : - FlexibleAdapter>(null, controller, true) { +class ExtensionAdapter(val listener: OnButtonClickListener) : + FlexibleAdapter>(null, listener, true) { - val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) + val cardBackground = (listener as ExtensionBottomSheet).context.getResourceColor(R.attr + .background_card) init { setDisplayHeadersAtStartUp(true) @@ -22,7 +24,7 @@ class ExtensionAdapter(val controller: ExtensionController) : /** * Listener for browse item clicks. */ - val buttonClickListener: ExtensionAdapter.OnButtonClickListener = controller + val buttonClickListener: ExtensionAdapter.OnButtonClickListener = listener interface OnButtonClickListener { fun onButtonClick(position: Int) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt similarity index 53% rename from app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt index 04d2a8de3a..6144cebf40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt @@ -1,56 +1,74 @@ package eu.kanade.tachiyomi.ui.extension import android.app.Application -import android.os.Bundle import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.extension.ExtensionManager +import eu.kanade.tachiyomi.extension.ExtensionsChangedListener import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.system.LocaleHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.concurrent.TimeUnit -private typealias ExtensionTuple - = Triple, List, List> +typealias ExtensionTuple = + Triple, List, List> /** - * Presenter of [ExtensionController]. + * Presenter of [ExtensionBottomSheet]. */ -open class ExtensionPresenter( +class ExtensionBottomPresenter( + private val bottomSheet: ExtensionBottomSheet, private val extensionManager: ExtensionManager = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get() -) : BasePresenter() { +) : ExtensionsChangedListener { + private var scope = CoroutineScope(Job() + Dispatchers.Default) private var extensions = emptyList() private var currentDownloads = hashMapOf() - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + fun onCreate() { + scope.launch { + extensionManager.findAvailableExtensionsAsync() + extensions = toItems( + Triple( + extensionManager.installedExtensions, + extensionManager.untrustedExtensions, + extensionManager.availableExtensions + ) + ) + withContext(Dispatchers.Main) { bottomSheet.setExtensions(extensions) } + extensionManager.setListener(this@ExtensionBottomPresenter) + } + } - extensionManager.findAvailableExtensions() - bindToExtensionsObservable() + fun onDestroy() { + extensionManager.removeListener(this) + } + + fun refreshExtensions() { + scope.launch { + extensions = toItems( + Triple( + extensionManager.installedExtensions, + extensionManager.untrustedExtensions, + extensionManager.availableExtensions + ) + ) + withContext(Dispatchers.Main) { bottomSheet.setExtensions(extensions) } + } } - private fun bindToExtensionsObservable(): Subscription { - val installedObservable = extensionManager.getInstalledExtensionsObservable() - val untrustedObservable = extensionManager.getUntrustedExtensionsObservable() - val availableObservable = extensionManager.getAvailableExtensionsObservable() - .startWith(emptyList()) - - return Observable.combineLatest(installedObservable, untrustedObservable, availableObservable) - { installed, untrusted, available -> Triple(installed, untrusted, available) } - .debounce(100, TimeUnit.MILLISECONDS) - .map(::toItems) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, _ -> view.setExtensions(extensions) }) + override fun extensionsUpdated() { + refreshExtensions() } @Synchronized @@ -65,14 +83,14 @@ open class ExtensionPresenter( val installedSorted = installed.sortedWith(compareBy({ !it.hasUpdate }, { !it.isObsolete }, { it.pkgName })) val untrustedSorted = untrusted.sortedBy { it.pkgName } val availableSorted = available - // Filter out already installed extensions and disabled languages - .filter { avail -> installed.none { it.pkgName == avail.pkgName } - && untrusted.none { it.pkgName == avail.pkgName } - && (avail.lang in activeLangs || avail.lang == "all")} - .sortedBy { it.pkgName } + // Filter out already installed extensions and disabled languages + .filter { avail -> installed.none { it.pkgName == avail.pkgName } && + untrusted.none { it.pkgName == avail.pkgName } && + (avail.lang in activeLangs || avail.lang == "all") } + .sortedBy { it.pkgName } if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) { - val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size) + val header = ExtensionGroupItem(context.getString(R.string.installed), installedSorted.size + untrustedSorted.size) items += installedSorted.map { extension -> ExtensionItem(extension, header, currentDownloads[extension.pkgName]) } @@ -82,22 +100,25 @@ open class ExtensionPresenter( } if (availableSorted.isNotEmpty()) { val availableGroupedByLang = availableSorted - .groupBy { LocaleHelper.getDisplayName(it.lang, context) } - .toSortedMap() + .groupBy { LocaleHelper.getDisplayName(it.lang, context) } + .toSortedMap() availableGroupedByLang - .forEach { - val header = ExtensionGroupItem(it.key, it.value.size) - items += it.value.map { extension -> - ExtensionItem(extension, header, currentDownloads[extension.pkgName]) - } + .forEach { + val header = ExtensionGroupItem(it.key, it.value.size) + items += it.value.map { extension -> + ExtensionItem(extension, header, currentDownloads[extension.pkgName]) } + } } this.extensions = items return items } + fun getExtensionUpdateCount(): Int = preferences.extensionUpdatesCount().getOrDefault() + fun getAutoCheckPref() = preferences.automaticExtUpdates() + @Synchronized private fun updateInstallStep(extension: Extension, state: InstallStep): ExtensionItem? { val extensions = extensions.toMutableList() @@ -124,13 +145,13 @@ open class ExtensionPresenter( private fun Observable.subscribeToInstallUpdate(extension: Extension) { this.doOnNext { currentDownloads[extension.pkgName] = it } - .doOnUnsubscribe { currentDownloads.remove(extension.pkgName) } - .map { state -> updateInstallStep(extension, state) } - .subscribeWithView({ view, item -> - if (item != null) { - view.downloadUpdate(item) - } - }) + .doOnUnsubscribe { currentDownloads.remove(extension.pkgName) } + .map { state -> updateInstallStep(extension, state) } + .subscribe { item -> + if (item != null) { + bottomSheet.downloadUpdate(item) + } + } } fun uninstallExtension(pkgName: String) { @@ -144,5 +165,4 @@ open class ExtensionPresenter( fun trustSignature(signatureHash: String) { extensionManager.trustSignature(signatureHash) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt new file mode 100644 index 0000000000..e3019f6158 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt @@ -0,0 +1,239 @@ +package eu.kanade.tachiyomi.ui.extension + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.LinearLayout +import com.f2prateek.rx.preferences.Preference +import com.google.android.material.bottomsheet.BottomSheetBehavior +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import kotlinx.android.synthetic.main.extensions_bottom_sheet.view.* + +class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : +LinearLayout(context, attrs), +ExtensionAdapter.OnButtonClickListener, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ExtensionTrustDialog.Listener { + + var sheetBehavior: BottomSheetBehavior<*>? = null + private lateinit var autoCheckItem: AutoCheckItem + + var shouldCallApi = false + + /** + * Adapter containing the list of extensions + */ + private var adapter: FlexibleAdapter>? = null + + val presenter = ExtensionBottomPresenter(this) + + private var extensions: List = emptyList() + + lateinit var controller: CatalogueController + + fun onCreate(controller: CatalogueController) { + // Initialize adapter, scroll listener and recycler views + autoCheckItem = AutoCheckItem(presenter.getAutoCheckPref()) + adapter = ExtensionAdapter(this) + sheetBehavior = BottomSheetBehavior.from(this) + // Create recycler and set adapter. + ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + ext_recycler.adapter = adapter + ext_recycler.setHasFixedSize(true) + ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(context)) + ext_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + this.controller = controller + presenter.onCreate() + updateExtTitle() + + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = context.obtainStyledAttributes(attrsArray) + val headerHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + ext_recycler.doOnApplyWindowInsets { _, windowInsets, _ -> + ext_recycler.updateLayoutParams { + topMargin = windowInsets.systemWindowInsetTop + headerHeight - + (sheet_layout.height) + } + } + sheet_layout.setOnClickListener { + if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + fetchOnlineExtensionsIfNeeded() + } else { + sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + presenter.getExtensionUpdateCount() + } + + fun fetchOnlineExtensionsIfNeeded() { + if (shouldCallApi) { + presenter.findAvailableExtensions() + shouldCallApi = false + } + } + + fun updateExtTitle() { + val extCount = presenter.getExtensionUpdateCount() + title_text.text = if (extCount == 0) context.getString(R.string.extensions) + else resources.getQuantityString(R.plurals.extension_updates_available, extCount, + extCount) + + title_text.setTextColor(context.getResourceColor( + if (extCount == 0) R.attr.actionBarTintColor else R.attr.colorAccent)) + } + + override fun onButtonClick(position: Int) { + val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return + when (extension) { + is Extension.Installed -> { + if (!extension.hasUpdate) { + openDetails(extension) + } else { + presenter.updateExtension(extension) + } + } + is Extension.Available -> { + presenter.installExtension(extension) + } + is Extension.Untrusted -> { + openTrustDialog(extension) + } + } + } + + override fun onItemClick(view: View?, position: Int): Boolean { + val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false + if (extension is Extension.Installed) { + openDetails(extension) + } else if (extension is Extension.Untrusted) { + openTrustDialog(extension) + } + + return false + } + + override fun onItemLongClick(position: Int) { + val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return + if (extension is Extension.Installed || extension is Extension.Untrusted) { + uninstallExtension(extension.pkgName) + } + } + + private fun openDetails(extension: Extension.Installed) { + val controller = ExtensionDetailsController(extension.pkgName) + this.controller.router.pushController(controller.withFadeTransaction()) + } + + private fun openTrustDialog(extension: Extension.Untrusted) { + ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName) + .showDialog(controller.router) + } + + fun setExtensions(extensions: List) { + this.extensions = extensions + controller.presenter.updateSources() + drawExtensions() + } + + fun drawExtensions() { + if (!controller.extQuery.isBlank()) { + adapter?.updateDataSet( + extensions.filter { + it.extension.name.contains(controller.extQuery, ignoreCase = true) + }) + } else { + adapter?.updateDataSet(extensions) + } + updateExtTitle() + setLastUsedSource() + } + + /** + * Called to set the last used catalogue at the top of the view. + */ + private fun setLastUsedSource() { + adapter?.removeAllScrollableHeaders() + adapter?.addScrollableHeader(autoCheckItem) + } + + fun downloadUpdate(item: ExtensionItem) { + adapter?.updateItem(item, item.installStep) + } + + override fun trustSignature(signatureHash: String) { + presenter.trustSignature(signatureHash) + } + + override fun uninstallExtension(pkgName: String) { + presenter.uninstallExtension(pkgName) + } +} + +class AutoCheckItem(private val autoCheck: Preference) : AbstractHeaderItem() { + + override fun getLayoutRes(): Int { + return R.layout.auto_ext_checkbox + } + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): AutoCheckHolder { + return AutoCheckHolder(view, adapter, autoCheck) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: AutoCheckHolder, + position: Int, + payloads: MutableList? + ) { + // holder.bind(autoCheck.getOrDefault()) + } + + override fun equals(other: Any?): Boolean { + return (this === other) + } + + override fun hashCode(): Int { + return -1 + } + + class AutoCheckHolder( + val view: View, + private val adapter: FlexibleAdapter>, + autoCheck: Preference + ) : + FlexibleViewHolder(view, adapter, true) { + private val autoCheckbox: CheckBox = view.findViewById(R.id.auto_checkbox) + + init { + autoCheckbox.bindToPreference(autoCheck) + } + + /** + * Binds a checkbox or switch view with a boolean preference. + */ + private fun CompoundButton.bindToPreference(pref: Preference) { + isChecked = pref.getOrDefault() + setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionController.kt deleted file mode 100644 index bac22f714b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionController.kt +++ /dev/null @@ -1,221 +0,0 @@ -package eu.kanade.tachiyomi.ui.extension - -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.widget.SearchView -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.FadeChangeHandler -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges -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.getOrDefault -import eu.kanade.tachiyomi.extension.ExtensionUpdateJob -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.util.view.RecyclerWindowInsetsListener -import kotlinx.android.synthetic.main.extension_controller.* -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -/** - * Controller to manage the catalogues available in the app. - */ -open class ExtensionController : NucleusController(), - ExtensionAdapter.OnButtonClickListener, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - ExtensionTrustDialog.Listener { - - /** - * Adapter containing the list of manga from the catalogue. - */ - private var adapter: FlexibleAdapter>? = null - - private var extensions: List = emptyList() - - private var query = "" - - init { - setHasOptionsMenu(true) - } - - override fun getTitle(): String? { - return applicationContext?.getString(R.string.label_extensions) - } - - override fun createPresenter(): ExtensionPresenter { - return ExtensionPresenter() - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.extension_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - ext_swipe_refresh.isRefreshing = true - ext_swipe_refresh.refreshes().subscribeUntilDestroy { - presenter.findAvailableExtensions() - } - - // Initialize adapter, scroll listener and recycler views - adapter = ExtensionAdapter(this) - // Create recycler and set adapter. - ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context) - ext_recycler.adapter = adapter - ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context)) - ext_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_search -> expandActionViewFromInteraction = true - R.id.action_filter -> { - router.pushController((RouterTransaction.with(SettingsExtensionsController())) - .popChangeHandler(SettingsExtensionsFadeChangeHandler()) - .pushChangeHandler(FadeChangeHandler())) - } - R.id.action_auto_check -> { - item.isChecked = !item.isChecked - val preferences:PreferencesHelper = Injekt.get() - preferences.automaticExtUpdates().set(item.isChecked) - - if (item.isChecked) - ExtensionUpdateJob.setupTask() - else - ExtensionUpdateJob.cancelTask() - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (!type.isPush && handler is SettingsExtensionsFadeChangeHandler) { - presenter.findAvailableExtensions() - } - } - - override fun onButtonClick(position: Int) { - val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return - when (extension) { - is Extension.Installed -> { - if (!extension.hasUpdate) { - openDetails(extension) - } else { - presenter.updateExtension(extension) - } - } - is Extension.Available -> { - presenter.installExtension(extension) - } - is Extension.Untrusted -> { - openTrustDialog(extension) - } - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.extension_main, menu) - - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - searchView.maxWidth = Int.MAX_VALUE - - if (query.isNotEmpty()) { - searchItem.expandActionView() - searchView.setQuery(query, true) - searchView.clearFocus() - } - - searchView.queryTextChanges() - .filter { router.backstack.lastOrNull()?.controller() == this } - .subscribeUntilDestroy { - query = it.toString() - drawExtensions() - } - - // Fixes problem with the overflow icon showing up in lieu of search - searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() }) - - val autoItem = menu.findItem(R.id.action_auto_check) - val preferences:PreferencesHelper = Injekt.get() - autoItem.isChecked = preferences.automaticExtUpdates().getOrDefault() - } - - override fun onItemClick(view: View?, position: Int): Boolean { - val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false - if (extension is Extension.Installed) { - openDetails(extension) - } else if (extension is Extension.Untrusted) { - openTrustDialog(extension) - } - - return false - } - - override fun onItemLongClick(position: Int) { - val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return - if (extension is Extension.Installed || extension is Extension.Untrusted) { - uninstallExtension(extension.pkgName) - } - } - - private fun openDetails(extension: Extension.Installed) { - val controller = ExtensionDetailsController(extension.pkgName) - router.pushController(controller.withFadeTransaction()) - } - - private fun openTrustDialog(extension: Extension.Untrusted) { - ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName) - .showDialog(router) - } - - fun setExtensions(extensions: List) { - ext_swipe_refresh?.isRefreshing = false - this.extensions = extensions - drawExtensions() - } - - fun drawExtensions() { - if (!query.isBlank()) { - adapter?.updateDataSet( - extensions.filter { - it.extension.name.contains(query, ignoreCase = true) - }) - } else { - adapter?.updateDataSet(extensions) - } - } - - fun downloadUpdate(item: ExtensionItem) { - adapter?.updateItem(item, item.installStep) - } - - override fun trustSignature(signatureHash: String) { - presenter.trustSignature(signatureHash) - } - - override fun uninstallExtension(pkgName: String) { - presenter.uninstallExtension(pkgName) - } - - class SettingsExtensionsFadeChangeHandler : FadeChangeHandler() -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsController.kt index 1695767c3b..faedb06e1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsController.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.extension import android.annotation.SuppressLint -import android.app.Dialog import android.content.Context import android.os.Bundle import android.util.TypedValue @@ -30,9 +29,10 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.setting.preferenceCategory +import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController import eu.kanade.tachiyomi.widget.preference.ListMatPreference -import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog import kotlinx.android.synthetic.main.extension_detail_controller.* @@ -63,19 +63,20 @@ class ExtensionDetailsController(bundle: Bundle? = null) : } override fun getTitle(): String? { - return resources?.getString(R.string.label_extension_info) + return resources?.getString(R.string.extension_info) } @SuppressLint("PrivateResource") override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForController() val extension = presenter.extension ?: return val context = view.context extension_title.text = extension.name - extension_version.text = context.getString(R.string.ext_version_info, extension.versionName) - extension_lang.text = context.getString(R.string.ext_language_info, LocaleHelper.getDisplayName(extension.lang, context)) + extension_version.text = context.getString(R.string.version_, extension.versionName) + extension_lang.text = context.getString(R.string.language_, LocaleHelper.getDisplayName(extension.lang, context)) extension_pkg.text = extension.pkgName extension.getApplicationIcon(context)?.let { extension_icon.setImageDrawable(it) } extension_uninstall_button.clicks().subscribeUntilDestroy { @@ -111,7 +112,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : if (screen.preferenceCount == 0) { extension_prefs_empty_view.show(R.drawable.ic_no_settings, - R.string.ext_empty_preferences) + R.string.empty_preferences_for_extension) } } @@ -222,5 +223,4 @@ class ExtensionDetailsController(bundle: Bundle? = null) : const val PKGNAME_KEY = "pkg_name" const val LASTOPENPREFERENCE_KEY = "last_open_preference" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsPresenter.kt index 1b9b958f06..93b79d4670 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsPresenter.kt @@ -8,8 +8,8 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionDetailsPresenter( - val pkgName: String, - private val extensionManager: ExtensionManager = Injekt.get() + val pkgName: String, + private val extensionManager: ExtensionManager = Injekt.get() ) : BasePresenter() { val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt index cc99508e31..c843626a92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDividerItemDecoration.kt @@ -4,7 +4,6 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.Drawable -import androidx.recyclerview.widget.RecyclerView import android.view.View class ExtensionDividerItemDecoration(context: Context) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() { @@ -36,9 +35,12 @@ class ExtensionDividerItemDecoration(context: Context) : androidx.recyclerview.w } } - override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView, - state: androidx.recyclerview.widget.RecyclerView.State) { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: androidx.recyclerview.widget.RecyclerView, + state: androidx.recyclerview.widget.RecyclerView.State + ) { outRect.set(0, 0, 0, divider.intrinsicHeight) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt index 01a53c647d..49234aae4b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt @@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.extension import android.annotation.SuppressLint import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import kotlinx.android.synthetic.main.extension_card_header.title import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlinx.android.synthetic.main.extension_card_header.* class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter>) : BaseFlexibleViewHolder(view, adapter) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt index 36923297c6..186ef930ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt @@ -32,8 +32,12 @@ data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderI /** * Binds this item to the given view holder. */ - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ExtensionGroupHolder, - position: Int, payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: ExtensionGroupHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(this) } @@ -49,5 +53,4 @@ data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderI override fun hashCode(): Int { return name.hashCode() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt index 5e92335b4a..8e4a8039e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt @@ -1,8 +1,9 @@ package eu.kanade.tachiyomi.ui.extension +import android.content.res.ColorStateList +import android.graphics.Color import android.view.View import androidx.core.content.ContextCompat -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.extension.model.Extension @@ -10,6 +11,8 @@ import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.resetStrokeColor import io.github.mthli.slice.Slice import kotlinx.android.synthetic.main.extension_card_item.* @@ -40,16 +43,16 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) : lang.text = if (extension !is Extension.Untrusted) { LocaleHelper.getDisplayName(extension.lang, itemView.context) } else { - itemView.context.getString(R.string.ext_untrusted).toUpperCase() + itemView.context.getString(R.string.untrusted).toUpperCase() } - GlideApp.with(itemView.context).clear(image) + GlideApp.with(itemView.context).clear(edit_button) if (extension is Extension.Available) { GlideApp.with(itemView.context) .load(extension.iconUrl) - .into(image) + .into(edit_button) } else { - extension.getApplicationIcon(itemView.context)?.let { image.setImageDrawable(it) } + extension.getApplicationIcon(itemView.context)?.let { edit_button.setImageDrawable(it) } } bindButton(item) } @@ -60,19 +63,19 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) : isClickable = true isActivated = false - background = VectorDrawableCompat.create(resources!!, R.drawable.button_bg_transparent, null) - setTextColor(ContextCompat.getColorStateList(context, R.drawable.button_bg_transparent)) + setTextColor(ContextCompat.getColorStateList(context, R.drawable.button_text_state)) + backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) + resetStrokeColor() val extension = item.extension - val installStep = item.installStep if (installStep != null) { setText(when (installStep) { - InstallStep.Pending -> R.string.ext_pending - InstallStep.Downloading -> R.string.ext_downloading - InstallStep.Installing -> R.string.ext_installing - InstallStep.Installed -> R.string.ext_installed - InstallStep.Error -> R.string.action_retry + InstallStep.Pending -> R.string.pending + InstallStep.Downloading -> R.string.downloading + InstallStep.Installing -> R.string.installing + InstallStep.Installed -> R.string.installed + InstallStep.Error -> R.string.retry }) if (installStep != InstallStep.Error) { isEnabled = false @@ -82,24 +85,25 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) : when { extension.hasUpdate -> { isActivated = true - setText(R.string.ext_update) + backgroundTintList = ColorStateList.valueOf( + context.getResourceColor(R.attr.colorAccent)) + strokeColor = ColorStateList.valueOf(Color.TRANSPARENT) + setText(R.string.update) } extension.isObsolete -> { // Red outline - background = VectorDrawableCompat.create(resources, R.drawable.button_bg_error, null) setTextColor(ContextCompat.getColorStateList(context, R.drawable.button_bg_error)) - setText(R.string.ext_obsolete) + setText(R.string.obsolete) } else -> { - setText(R.string.ext_details) + setText(R.string.details) } } } else if (extension is Extension.Untrusted) { - setText(R.string.ext_trust) + setText(R.string.trust) } else { - setText(R.string.ext_install) + setText(R.string.install) } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionItem.kt index 3ea8b2a933..2fb20c0b19 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionItem.kt @@ -16,9 +16,11 @@ import eu.kanade.tachiyomi.source.CatalogueSource * @param source Instance of [CatalogueSource] containing source information. * @param header The header for this item. */ -data class ExtensionItem(val extension: Extension, - val header: ExtensionGroupItem? = null, - val installStep: InstallStep? = null) : +data class ExtensionItem( + val extension: Extension, + val header: ExtensionGroupItem? = null, + val installStep: InstallStep? = null +) : AbstractSectionableItem(header) { /** @@ -38,8 +40,12 @@ data class ExtensionItem(val extension: Extension, /** * Binds this item to the given view holder. */ - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: ExtensionHolder, - position: Int, payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: ExtensionHolder, + position: Int, + payloads: MutableList? + ) { if (payloads == null || payloads.isEmpty()) { holder.bind(this) @@ -57,5 +63,4 @@ data class ExtensionItem(val extension: Extension, override fun hashCode(): Int { return extension.pkgName.hashCode() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt index 577b27fd0a..fcae6b480d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt @@ -3,29 +3,29 @@ package eu.kanade.tachiyomi.ui.extension import android.app.Dialog import android.os.Bundle import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.controller.DialogController class ExtensionTrustDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T: ExtensionTrustDialog.Listener { + where T : ExtensionTrustDialog.Listener { + lateinit var listener: Listener constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply { putString(SIGNATURE_KEY, signatureHash) putString(PKGNAME_KEY, pkgName) }) { - targetController = target + listener = target } override fun onCreateDialog(savedViewState: Bundle?): Dialog { return MaterialDialog(activity!!) .title(R.string.untrusted_extension) .message(R.string.untrusted_extension_message) - .positiveButton(R.string.ext_trust) { - (targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!) + .positiveButton(R.string.trust) { + listener.trustSignature(args.getString(SIGNATURE_KEY)!!) } - .negativeButton(R.string.ext_uninstall) { - (targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!) + .negativeButton(R.string.uninstall) { + listener.uninstallExtension(args.getString(PKGNAME_KEY)!!) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt index 61d0b71a9a..bc3c565743 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/SettingsExtensionsController.kt @@ -12,10 +12,10 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SettingsExtensionsController: SettingsController() { +class SettingsExtensionsController : SettingsController() { override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.action_filter + titleRes = R.string.filter val activeLangs = preferences.enabledLanguages().getOrDefault() @@ -49,4 +49,4 @@ class SettingsExtensionsController: SettingsController() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt index d2f56131fb..ed46df504d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt @@ -19,8 +19,12 @@ class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : private var preselected = emptyArray() - constructor(target: T, mangas: List, categories: List, - preselected: Array) : this() { + constructor( + target: T, + mangas: List, + categories: List, + preselected: Array + ) : this() { this.mangas = mangas this.categories = categories @@ -30,7 +34,7 @@ class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : override fun onCreateDialog(savedViewState: Bundle?): Dialog { return MaterialDialog(activity!!) - .title(R.string.action_move_category) + .title(R.string.move_to_categories) .listItemsMultiChoice( items = categories.map { it.name }, initialSelection = preselected.toIntArray(), @@ -46,5 +50,4 @@ class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : interface Listener { fun updateCategoriesForMangas(mangas: List, categories: List) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DisplayBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DisplayBottomSheet.kt new file mode 100644 index 0000000000..1cf9728375 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DisplayBottomSheet.kt @@ -0,0 +1,126 @@ +package eu.kanade.tachiyomi.ui.library + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.RadioButton +import android.widget.RadioGroup +import com.f2prateek.rx.preferences.Preference +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.display_bottom_sheet.* +import uy.kohesive.injekt.injectLazy + +class DisplayBottomSheet(private val controller: LibraryController) : BottomSheetDialog + (controller.activity!!, R.style.BottomSheetDialogTheme) { + + val activity = controller.activity!! + + /** + * Preferences helper. + */ + private val preferences by injectLazy() + + private var sheetBehavior: BottomSheetBehavior<*> + + init { + // Use activity theme for this layout + val view = activity.layoutInflater.inflate(R.layout.display_bottom_sheet, null) + setContentView(view) + + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) + setEdgeToEdge(activity, view) + val height = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + sheetBehavior.peekHeight = 220.dpToPx + height + + sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { } + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.skipCollapsed = true + } + } + }) + } + + override fun onStart() { + super.onStart() + sheetBehavior.skipCollapsed = true + sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + /** + * Called when the sheet is created. It initializes the listeners and values of the preferences. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initGeneralPreferences() + setBottomEdge(hide_filters, activity) + close_button.setOnClickListener { + dismiss() + true + } + settings_scroll_view.viewTreeObserver.addOnGlobalLayoutListener { + val isScrollable = + settings_scroll_view!!.height < bottom_sheet.height + + settings_scroll_view.paddingTop + settings_scroll_view.paddingBottom + close_button.visibleIf(isScrollable) + } + } + + private fun initGeneralPreferences() { + display_group.bindToPreference(preferences.libraryLayout()) { + controller.reattachAdapter() + if (sheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) + dismiss() + } + uniform_grid.bindToPreference(preferences.uniformGrid()) { + controller.reattachAdapter() + } + autohide_seeker.bindToPreference(preferences.alwaysShowSeeker()) { + controller.updateShowScrollbar(autohide_seeker.isChecked) + } + grid_size_toggle_group.bindToPreference(preferences.gridSize()) { + controller.reattachAdapter() + } + download_badge.bindToPreference(preferences.downloadBadge()) { + controller.presenter.requestDownloadBadgesUpdate() + } + unread_badge_group.bindToPreference(preferences.unreadBadgeType()) { + controller.presenter.requestUnreadBadgesUpdate() + } + hide_filters.bindToPreference(preferences.hideFiltersAtStart()) + } + + /** + * Binds a checkbox or switch view with a boolean preference. + */ + private fun CompoundButton.bindToPreference(pref: Preference, block: (() -> Unit)? = null) { + isChecked = pref.getOrDefault() + setOnCheckedChangeListener { _, isChecked -> + pref.set(isChecked) + block?.invoke() + } + } + + /** + * Binds a radio group with a int preference. + */ + private fun RadioGroup.bindToPreference(pref: Preference, block: (() -> Unit)? = null) { + (getChildAt(pref.getOrDefault()) as RadioButton).isChecked = true + setOnCheckedChangeListener { _, checkedId -> + val index = indexOfChild(findViewById(checkedId)) + pref.set(index) + block?.invoke() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt deleted file mode 100644 index 9eb0c9531b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt +++ /dev/null @@ -1,103 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter - -/** - * This adapter stores the categories from the library, used with a ViewPager. - * - * @constructor creates an instance of the adapter. - */ -class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() { - - /** - * The categories to bind in the adapter. - */ - var categories: List = emptyList() - // This setter helps to not refresh the adapter if the reference to the list doesn't change. - set(value) { - if (field !== value) { - field = value - notifyDataSetChanged() - } - } - - private var boundViews = arrayListOf() - - /** - * Creates a new view for this adapter. - * - * @return a new view. - */ - override fun createView(container: ViewGroup): View { - val view = container.inflate(R.layout.library_category) as LibraryCategoryView - view.onCreate(controller) - return view - } - - /** - * Binds a view with a position. - * - * @param view the view to bind. - * @param position the position in the adapter. - */ - override fun bindView(view: View, position: Int) { - (view as LibraryCategoryView).onBind(categories[position]) - boundViews.add(view) - } - - /** - * Recycles a view. - * - * @param view the view to recycle. - * @param position the position in the adapter. - */ - override fun recycleView(view: View, position: Int) { - (view as LibraryCategoryView).onRecycle() - boundViews.remove(view) - } - - /** - * Returns the number of categories. - * - * @return the number of categories or 0 if the list is null. - */ - override fun getCount(): Int { - return categories.size - } - - /** - * Returns the title to display for a category. - * - * @param position the position of the element. - * @return the title to display. - */ - override fun getPageTitle(position: Int): CharSequence { - return categories[position].name - } - - /** - * Returns the position of the view. - */ - override fun getItemPosition(obj: Any): Int { - val view = obj as? LibraryCategoryView ?: return POSITION_NONE - val index = categories.indexOfFirst { it.id == view.category.id } - return if (index == -1) POSITION_NONE else index - } - - /** - * Called when the view of this adapter is being destroyed. - */ - fun onDestroy() { - for (view in boundViews) { - if (view is LibraryCategoryView) { - view.unsubscribe() - } - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryBadge.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryBadge.kt new file mode 100644 index 0000000000..0e45d9b006 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryBadge.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import com.google.android.material.card.MaterialCardView +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.updatePaddingRelative +import kotlinx.android.synthetic.main.unread_download_badge.view.* + +class LibraryBadge @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + MaterialCardView(context, attrs) { + + fun setUnreadDownload(unread: Int, downloads: Int, showTotalChapters: Boolean) { + // Update the unread count and its visibility. + with(unread_text) { + text = if (unread == -1) "0" else unread.toString() + setTextColor(if (unread == -1 && !showTotalChapters) + context.getResourceColor(android.R.attr.colorAccent) + else Color.WHITE) + setBackgroundColor( + if (showTotalChapters) ContextCompat.getColor(context, R.color.material_deep_purple_500) + else context.getResourceColor(android.R.attr.colorAccent)) + visibility = when { + unread > 0 || unread == -1 || showTotalChapters -> View.VISIBLE + else -> View.GONE + } + } + + // Update the download count or local status and its visibility. + with(download_text) { + visibility = if (downloads == -2 || downloads > 0) View.VISIBLE else View.GONE + text = if (downloads == -2) + resources.getString(R.string.local) + else downloads.toString() + } + + // Show the bade card if unread or downloads exists + visibility = if (download_text.visibility == View.VISIBLE || unread_text + .visibility != View.GONE) View.VISIBLE else View.GONE + + // Show the angles divider if both unread and downloads exists + unread_angle.visibility = if (download_text.visibility == View.VISIBLE && unread_text + .visibility != View.GONE) View.VISIBLE else View.GONE + unread_angle.setColorFilter( + if (showTotalChapters) ContextCompat.getColor(context, R.color.material_deep_purple_500) + else context.getResourceColor(android.R.attr.colorAccent)) + if (unread_angle.visibility == View.VISIBLE) { + download_text.updatePaddingRelative(end = 8.dpToPx) + unread_text.updatePaddingRelative(start = 2.dpToPx) + } else { + download_text.updatePaddingRelative(end = 5.dpToPx) + unread_text.updatePaddingRelative(start = 5.dpToPx) + } + } + + fun setChapters(chapters: Int?) { + setUnreadDownload(chapters ?: 0, 0, chapters != null) + } + + fun setInLibrary(inLibrary: Boolean) { + visibility = if (inLibrary) View.VISIBLE else View.GONE + unread_angle.visibility = View.GONE + unread_text.updatePaddingRelative(start = 5.dpToPx) + unread_text.visibility = if (inLibrary) View.VISIBLE else View.GONE + unread_text.text = resources.getText(R.string.in_library) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 174e3512f8..c039945425 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -2,33 +2,35 @@ package eu.kanade.tachiyomi.ui.library import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.category.CategoryAdapter -import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.removeArticles import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat -import java.util.* - +import java.util.Calendar +import java.util.Date +import java.util.Locale +import kotlin.math.max /** * Adapter storing a list of manga in a certain category. * * @param view the fragment containing this adapter. */ -class LibraryCategoryAdapter(val view: LibraryCategoryView) : - FlexibleAdapter(null, view, true) { +class LibraryCategoryAdapter(val libraryListener: LibraryListener) : + FlexibleAdapter>(null, libraryListener, true) { + init { + setDisplayHeadersAtStartUp(true) + } /** * The list of manga in this category. */ private var mangas: List = emptyList() - val onItemReleaseListener: CategoryAdapter.OnItemReleaseListener = view - /** * Sets a list of manga in the adapter. * @@ -41,84 +43,210 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) : performFilter() } + /** + * Returns the position in the adapter for the given manga. + * + * @param manga the manga to find. + */ + fun indexOf(categoryOrder: Int): Int { + return currentItems.indexOfFirst { + if (it is LibraryHeaderItem) it.category.order == categoryOrder + else false } + } + /** * Returns the position in the adapter for the given manga. * * @param manga the manga to find. */ fun indexOf(manga: Manga): Int { - return currentItems.indexOfFirst { it.manga.id == manga.id } + return currentItems.indexOfFirst { + if (it is LibraryItem) it.manga.id == manga.id + else false } + } + + fun getHeaderPositions(): List { + return currentItems.mapIndexedNotNull { index, it -> + if (it is LibraryHeaderItem) index + else null } + } + + /** + * Returns the position in the adapter for the given manga. + * + * @param manga the manga to find. + */ + fun allIndexOf(manga: Manga): List { + return currentItems.mapIndexedNotNull { index, it -> + if (it is LibraryItem && it.manga.id == manga.id) index + else null } } fun performFilter() { val s = getFilter(String::class.java) if (s.isNullOrBlank()) { updateDataSet(mangas) - } - else { + } else { updateDataSet(mangas.filter { it.filter(s) }) } - isLongPressDragEnabled = view.canDrag() && s.isNullOrBlank() + isLongPressDragEnabled = libraryListener.canDrag() && s.isNullOrBlank() } - override fun onCreateBubbleText(position: Int):String { - return if (position < scrollableHeaders.size) { - "Top" - } else if (position >= itemCount - scrollableFooters.size) { - "Bottom" - } else { // Get and show the first character - val iFlexible: IFlexible<*>? = getItem(position) - val preferences:PreferencesHelper by injectLazy() - when (preferences.librarySortingMode().getOrDefault()) { - LibrarySort.DRAG_AND_DROP -> { - if (!preferences.hideCategories().getOrDefault()) { - val title = (iFlexible as LibraryItem).manga.currentTitle() - if (preferences.removeArticles().getOrDefault()) - title.removeArticles().substring(0, 1).toUpperCase(Locale.US) - else title.substring(0, 1).toUpperCase(Locale.US) + fun getSectionText(position: Int): String? { + val preferences: PreferencesHelper by injectLazy() + val db: DatabaseHelper by injectLazy() + if (position == itemCount - 1) return "-" + val sorting = if (preferences.hideCategories().getOrDefault()) + preferences.hideCategories().getOrDefault() + else (headerItems.firstOrNull() as? LibraryHeaderItem)?.category?.sortingMode() + ?: LibrarySort.DRAG_AND_DROP + return when (val item: IFlexible<*>? = getItem(position)) { + is LibraryHeaderItem -> + if (preferences.hideCategories().getOrDefault() || item.category.id == 0) null + else item.category.name.first().toString() + + "\u200B".repeat(max(0, item.category.order)) + is LibraryItem -> { + when (sorting) { + LibrarySort.DRAG_AND_DROP -> { + val category = db.getCategoriesForManga(item.manga).executeAsBlocking() + .firstOrNull() + if (category == null) null + else getFirstLetter(category.name) + "\u200B".repeat(max(0, category.order)) } - else { - val db:DatabaseHelper by injectLazy() - val category = db.getCategoriesForManga((iFlexible as LibraryItem).manga) - .executeAsBlocking().firstOrNull()?.name - category?.chop(10) ?: "Default" + LibrarySort.LAST_READ -> { + val id = item.manga.id ?: return "" + val history = db.getHistoryByMangaId(id).executeAsBlocking() + val last = history.maxBy { it.last_read } + if (last != null && last.last_read > 100) getShorterDate(Date(last.last_read)) + else "*" + } + LibrarySort.TOTAL -> { + val unread = item.chapterCount + (unread / 100).toString() + } + LibrarySort.UNREAD -> { + val unread = item.manga.unread + if (unread > 0) (unread / 100).toString() + else "R" + } + LibrarySort.LATEST_CHAPTER -> { + val lastUpdate = item.manga.last_update + if (lastUpdate > 0) getShorterDate(Date(lastUpdate)) + else "*" + } + LibrarySort.DATE_ADDED -> { + val lastUpdate = item.manga.date_added + if (lastUpdate > 0) getShorterDate(Date(lastUpdate)) + else "*" + } + else -> { + val title = if (preferences.removeArticles() + .getOrDefault() + ) item.manga.title.removeArticles() + else item.manga.title + getFirstLetter(title) } } - LibrarySort.LAST_READ -> { - val db:DatabaseHelper by injectLazy() - val id = (iFlexible as LibraryItem).manga.id ?: return "" - val history = db.getHistoryByMangaId(id).executeAsBlocking() - val last = history.maxBy { it.last_read } - if (last != null) - getShortDate(Date(last.last_read)) - else - "N/A" - } - LibrarySort.UNREAD -> { - val unread = (iFlexible as LibraryItem).manga.unread - if (unread > 0) - unread.toString() - else - "Read" - } - LibrarySort.LAST_UPDATED -> { - val lastUpdate = (iFlexible as LibraryItem).manga.last_update - if (lastUpdate > 0) - getShortDate(Date(lastUpdate)) - else - "N/A" - } - else -> { - val title = (iFlexible as LibraryItem).manga.currentTitle() - if (preferences.removeArticles().getOrDefault()) - title.removeArticles().substring(0, 1).toUpperCase(Locale.US) - else title.substring(0, 1).toUpperCase(Locale.US) + } + else -> "" + } + } + + private fun getFirstLetter(name: String): String { + val letter = name.first() + return if (letter.isLetter()) letter.toString() + .toUpperCase(Locale.ROOT) else "#" + } + + override fun onCreateBubbleText(position: Int): String { + val preferences: PreferencesHelper by injectLazy() + val db: DatabaseHelper by injectLazy() + if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom) + return when (val iFlexible: IFlexible<*>? = getItem(position)) { + is LibraryHeaderItem -> + if (!preferences.hideCategories().getOrDefault()) iFlexible.category.name + else recyclerView.context.getString(R.string.top) + is LibraryItem -> { + if (iFlexible.manga.isBlank()) "" + else when (preferences.librarySortingMode().getOrDefault()) { + LibrarySort.DRAG_AND_DROP -> { + if (!preferences.hideCategories().getOrDefault()) { + val title = iFlexible.manga.title + if (preferences.removeArticles().getOrDefault()) title.removeArticles() + .substring(0, 1).toUpperCase(Locale.US) + else title.substring(0, 1).toUpperCase(Locale.US) + } else { + val category = db.getCategoriesForManga(iFlexible.manga) + .executeAsBlocking().firstOrNull()?.name + category ?: recyclerView.context.getString(R.string.default_value) + } + } + LibrarySort.LAST_READ -> { + val id = iFlexible.manga.id ?: return "" + val history = db.getHistoryByMangaId(id).executeAsBlocking() + val last = history.maxBy { it.last_read } + if (last != null && last.last_read > 100) getShortDate(Date(last.last_read)) + else "N/A" + } + LibrarySort.UNREAD -> { + val unread = iFlexible.manga.unread + if (unread > 0) getRange(unread) + else recyclerView.context.getString(R.string.read) + } + LibrarySort.TOTAL -> { + val total = iFlexible.chapterCount + if (total > 0) getRange(total) + else "N/A" + } + LibrarySort.LATEST_CHAPTER -> { + val lastUpdate = iFlexible.manga.last_update + if (lastUpdate > 0) getShortDate(Date(lastUpdate)) + else "N/A" + } + LibrarySort.DATE_ADDED -> { + val lastUpdate = iFlexible.manga.date_added + if (lastUpdate > 0) getShortDate(Date(lastUpdate)) + else "N/A" + } + else -> getSectionText(position) ?: "" } } + else -> "" + } + } + + private fun getRange(value: Int): String { + return when (value) { + in 1..99 -> "< 100" + in 100..199 -> "100-199" + in 200..299 -> "200-299" + in 300..399 -> "300-399" + in 400..499 -> "400-499" + in 500..599 -> "500-599" + in 600..699 -> "600-699" + in 700..799 -> "700-799" + in 800..899 -> "800-899" + in 900..Int.MAX_VALUE -> "900+" + else -> "None" } } - private fun getShortDate(date:Date):String { + private fun getShorterDate(date: Date): String { + val cal = Calendar.getInstance() + cal.time = Date() + + val yearNow = cal.get(Calendar.YEAR) + val cal2 = Calendar.getInstance() + cal2.time = date + val yearThen = cal2.get(Calendar.YEAR) + + return if (yearNow == yearThen) + SimpleDateFormat("M", Locale.getDefault()).format(date) + else + SimpleDateFormat("''yy", Locale.getDefault()).format(date) + } + + private fun getShortDate(date: Date): String { val cal = Calendar.getInstance() cal.time = Date() @@ -128,9 +256,21 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) : val yearThen = cal2.get(Calendar.YEAR) return if (yearNow == yearThen) - SimpleDateFormat("MMM", Locale.getDefault()).format(date) + SimpleDateFormat("MMMM", Locale.getDefault()).format(date) else SimpleDateFormat("yyyy", Locale.getDefault()).format(date) } + interface LibraryListener { + /** + * Called when an item of the list is released. + */ + fun startReading(position: Int) + fun onItemReleased(position: Int) + fun canDrag(): Boolean + fun updateCategory(catId: Int): Boolean + fun sortCategory(catId: Int, sortBy: Int) + fun selectAll(position: Int) + fun allSelected(position: Int): Boolean + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt deleted file mode 100644 index 305e42addf..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ /dev/null @@ -1,393 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.category.CategoryAdapter -import eu.kanade.tachiyomi.util.system.launchUI -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import eu.kanade.tachiyomi.util.lang.plusAssign -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import kotlinx.android.synthetic.main.library_category.view.* -import kotlinx.coroutines.delay -import rx.subscriptions.CompositeSubscription -import uy.kohesive.injekt.injectLazy - -/** - * Fragment containing the library manga for a certain category. - */ -class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FrameLayout(context, attrs), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.OnItemMoveListener, - CategoryAdapter.OnItemReleaseListener { - - /** - * Preferences. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * The fragment containing this view. - */ - private lateinit var controller: LibraryController - - private val db: DatabaseHelper by injectLazy() - /** - * Category for this view. - */ - lateinit var category: Category - private set - - /** - * Recycler view of the list of manga. - */ - private lateinit var recycler: RecyclerView - - /** - * Adapter to hold the manga in this category. - */ - private lateinit var adapter: LibraryCategoryAdapter - - /** - * Subscriptions while the view is bound. - */ - private var subscriptions = CompositeSubscription() - - private var lastTouchUpY = 0f - - private var lastClickPosition = -1 - - fun onCreate(controller: LibraryController) { - this.controller = controller - - recycler = if (preferences.libraryAsList().getOrDefault()) { - (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { - layoutManager = LinearLayoutManager(context) - } - } else { - (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { - spanCount = controller.mangaPerRow - } - } - - adapter = LibraryCategoryAdapter(this) - - recycler.setHasFixedSize(true) - recycler.adapter = adapter - swipe_refresh.addView(recycler) - adapter.fastScroller = fast_scroller - - fast_scroller.addOnScrollStateChangeListener { - controller.lockFilterBar(it) - } - recycler.doOnApplyWindowInsets { v, insets, padding -> - v.updatePaddingRelative(bottom = padding.bottom + insets.systemWindowInsetBottom) - - fast_scroller?.updateLayoutParams { - bottomMargin = insets.systemWindowInsetBottom - } - } - - // Double the distance required to trigger sync - swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) - swipe_refresh.setOnRefreshListener { - val inQueue = LibraryUpdateService.categoryInQueue(category.id) - controller.snack?.dismiss() - controller.snack = swipe_refresh.snack( - resources.getString( - when { - inQueue -> R.string.category_already_in_queue - LibraryUpdateService.isRunning(context) -> R.string.adding_category_to_queue - else -> R.string.updating_category_x - }, category.name)) - if (!inQueue) - LibraryUpdateService.start(context, category) - swipe_refresh.isRefreshing = false - } - } - - fun onBind(category: Category) { - this.category = category - - adapter.mode = if (controller.selectedMangas.isNotEmpty()) { - SelectableAdapter.Mode.MULTI - } else { - SelectableAdapter.Mode.SINGLE - } - adapter.isLongPressDragEnabled = canDrag() - - subscriptions += controller.searchRelay - .doOnNext { adapter.setFilter(it) } - .skip(1) - .subscribe { adapter.performFilter() } - - subscriptions += controller.libraryMangaRelay - .subscribe { onNextLibraryManga(it) } - - subscriptions += controller.selectionRelay - .subscribe { onSelectionChanged(it) } - - subscriptions += controller.selectAllRelay - .subscribe { - if (it == category.id) { - adapter.currentItems.forEach { item -> - controller.setSelection(item.manga, true) - } - controller.invalidateActionMode() - } - } - - subscriptions += controller.reorganizeRelay - .subscribe { - if (it.first == category.id) { - if (it.second in -2..-1) { - val items = adapter.currentItems.toMutableList() - val mangas = controller.selectedMangas - val selectedManga = items.filter { item -> item.manga in mangas } - items.removeAll(selectedManga) - if (it.second == -1) items.addAll(0, selectedManga) - else items.addAll(selectedManga) - adapter.setItems(items) - adapter.notifyDataSetChanged() - saveDragSort() - } - else { - category.mangaSort = ('a' + (it.second - 1)) - if (category.id == 0) - preferences.defaultMangaOrder().set(category.mangaSort.toString()) - else - db.insertCategory(category).asRxObservable().subscribe() - controller.enableReorderItems(category) - } - } - } - } - - fun canDrag(): Boolean { - val sortingMode = preferences.librarySortingMode().getOrDefault() - val filterOff = preferences.filterCompleted().getOrDefault() + - preferences.filterTracked().getOrDefault() + - preferences.filterUnread().getOrDefault() + - preferences.filterCompleted().getOrDefault() == 0 && - !preferences.hideCategories().getOrDefault() - return sortingMode == LibrarySort.DRAG_AND_DROP && filterOff && - adapter.mode != SelectableAdapter.Mode.MULTI - } - - fun onRecycle() { - adapter.setItems(emptyList()) - adapter.clearSelection() - unsubscribe() - } - - fun unsubscribe() { - subscriptions.clear() - } - - /** - * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the - * adapter. - * - * @param event the event received. - */ - fun onNextLibraryManga(event: LibraryMangaEvent) { - // Get the manga list for this category. - adapter.isLongPressDragEnabled = canDrag() - val mangaForCategory = event.getMangaForCategory(category).orEmpty() - - // Update the category with its manga. - adapter.setItems(mangaForCategory) - - swipe_refresh.isEnabled = !preferences.hideCategories().getOrDefault() - - if (adapter.mode == SelectableAdapter.Mode.MULTI) { - controller.selectedMangas.forEach { manga -> - val position = adapter.indexOf(manga) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() - } - } - } - } - - /** - * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection - * depending on the type of event received. - * - * @param event the selection event received. - */ - private fun onSelectionChanged(event: LibrarySelectionEvent) { - when (event) { - is LibrarySelectionEvent.Selected -> { - if (adapter.mode != SelectableAdapter.Mode.MULTI) { - adapter.mode = SelectableAdapter.Mode.MULTI - } - launchUI { - delay(100) - adapter.isLongPressDragEnabled = false - } - findAndToggleSelection(event.manga) - } - is LibrarySelectionEvent.Unselected -> { - findAndToggleSelection(event.manga) - if (adapter.indexOf(event.manga) != -1) lastClickPosition = -1 - if (controller.selectedMangas.isEmpty()) { - adapter.mode = SelectableAdapter.Mode.SINGLE - adapter.isLongPressDragEnabled = canDrag() - } - } - is LibrarySelectionEvent.Cleared -> { - adapter.mode = SelectableAdapter.Mode.SINGLE - adapter.clearSelection() - adapter.notifyDataSetChanged() - lastClickPosition = -1 - adapter.isLongPressDragEnabled = canDrag() - } - } - } - - /** - * Toggles the selection for the given manga and updates the view if needed. - * - * @param manga the manga to toggle. - */ - private fun findAndToggleSelection(manga: Manga) { - val position = adapter.indexOf(manga) - if (position != -1) { - adapter.toggleSelection(position) - (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation() - } - } - - /** - * Called when a manga is clicked. - * - * @param position the position of the element clicked. - * @return true if the item should be selected, false otherwise. - */ - override fun onItemClick(view: View?, position: Int): Boolean { - // If the action mode is created and the position is valid, toggle the selection. - val item = adapter.getItem(position) ?: return false - return if (adapter.mode == SelectableAdapter.Mode.MULTI) { - lastClickPosition = position - toggleSelection(position) - true - } else { - openManga(item.manga, lastTouchUpY) - false - } - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - when (ev?.action) { - MotionEvent.ACTION_UP -> lastTouchUpY = ev.y - } - return super.dispatchTouchEvent(ev) - } - - /** - * Called when a manga is long clicked. - * - * @param position the position of the element clicked. - */ - override fun onItemLongClick(position: Int) { - controller.createActionModeIfNeeded() - when { - lastClickPosition == -1 -> setSelection(position) - lastClickPosition > position -> for (i in position until lastClickPosition) - setSelection(i) - lastClickPosition < position -> for (i in lastClickPosition + 1..position) - setSelection(i) - else -> setSelection(position) - } - lastClickPosition = position - } - - override fun onItemMove(fromPosition: Int, toPosition: Int) { - - } - - override fun onItemReleased(position: Int) { - if (adapter.selectedItemCount == 0) saveDragSort() - } - - private fun saveDragSort() { - val mangaIds = adapter.currentItems.mapNotNull { it.manga.id } - category.mangaSort = null - category.mangaOrder = mangaIds - if (category.id == 0) - preferences.defaultMangaOrder().set(mangaIds.joinToString("/")) - else - db.insertCategory(category).asRxObservable().subscribe() - controller.onSortChanged() - controller.enableReorderItems(category) - } - override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { - if (adapter.selectedItemCount > 1) - return false - if (adapter.isSelected(fromPosition)) - toggleSelection(fromPosition) - return true - } - - override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { - val position = viewHolder?.adapterPosition ?: return - if (actionState == 2) onItemLongClick(position) - } - - /** - * Opens a manga. - * - * @param manga the manga to open. - */ - private fun openManga(manga: Manga, startY:Float?) { - controller.openManga(manga, startY) - } - - /** - * Tells the presenter to toggle the selection for the given position. - * - * @param position the position to toggle. - */ - private fun toggleSelection(position: Int) { - val item = adapter.getItem(position) ?: return - - controller.setSelection(item.manga, !adapter.isSelected(position)) - controller.invalidateActionMode() - } - - - /** - * Tells the presenter to set the selection for the given position. - * - * @param position the position to toggle. - */ - private fun setSelection(position: Int) { - val item = adapter.getItem(position) ?: return - - controller.setSelection(item.manga, true) - controller.invalidateActionMode() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index fbd50cda6d..26d2df1b9e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -1,89 +1,112 @@ package eu.kanade.tachiyomi.ui.library +import android.app.Activity import android.content.Context -import android.content.res.Configuration +import android.content.res.ColorStateList import android.graphics.Color import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.view.WindowInsets +import android.view.ViewPropertyAnimator import android.view.inputmethod.InputMethodManager +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.view.GravityCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.viewpager.widget.ViewPager +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import com.f2prateek.rx.preferences.Preference +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar -import com.google.android.material.tabs.TabLayout -import com.jakewharton.rxbinding.support.v4.view.pageSelections -import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay +import com.reddit.indicatorfastscroll.FastScrollItemIndicator +import com.reddit.indicatorfastscroll.FastScrollerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.library.LibraryServiceListener +import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController +import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.category.CategoryController +import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet +import eu.kanade.tachiyomi.ui.main.BottomSheetController import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.migration.MigrationController -import eu.kanade.tachiyomi.ui.migration.MigrationInterface +import eu.kanade.tachiyomi.ui.main.RootSearchInterface +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController -import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController -import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig -import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.marginTop +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.scrollViewWith +import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener +import eu.kanade.tachiyomi.util.view.setStyle import eu.kanade.tachiyomi.util.view.snack +import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import eu.kanade.tachiyomi.util.view.visible -import eu.kanade.tachiyomi.widget.ExtendedNavigationView -import kotlinx.android.synthetic.main.library_controller.* +import kotlinx.android.synthetic.main.filter_bottom_sheet.* +import kotlinx.android.synthetic.main.library_grid_recycler.* +import kotlinx.android.synthetic.main.library_list_controller.* import kotlinx.android.synthetic.main.main_activity.* -import rx.Subscription +import kotlinx.coroutines.delay import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.math.abs +import kotlin.math.roundToInt class LibraryController( - bundle: Bundle? = null, - private val preferences: PreferencesHelper = Injekt.get() -) : NucleusController(bundle), - TabbedController, - SecondaryDrawerController, - ActionMode.Callback, - ChangeMangaCategoriesDialog.Listener, - MigrationInterface { + bundle: Bundle? = null, + private val preferences: PreferencesHelper = Injekt.get() +) : BaseController(bundle), + ActionMode.Callback, + ChangeMangaCategoriesDialog.Listener, + FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, + FlexibleAdapter.OnItemMoveListener, LibraryCategoryAdapter.LibraryListener, + BottomSheetController, + RootSearchInterface, LibraryServiceListener { + + init { + setHasOptionsMenu(true) + retainViewMode = RetainViewMode.RETAIN_DETACH + } /** * Position of the active category. */ - var activeCategory: Int = preferences.lastUsedCategory().getOrDefault() - private set + private var activeCategory: Int = preferences.lastUsedCategory().getOrDefault() + + private var justStarted = true /** * Action mode for selections. */ private var actionMode: ActionMode? = null + private var libraryLayout: Int = preferences.libraryLayout().getOrDefault() + + private var singleCategory: Boolean = false + /** * Library search query. */ @@ -92,430 +115,794 @@ class LibraryController( /** * Currently selected mangas. */ - val selectedMangas = mutableSetOf() - - /** - * Current mangas to move. - */ - private var migratingMangas = mutableSetOf() - - /** - * Relay to notify the UI of selection updates. - */ - val selectionRelay: PublishRelay = PublishRelay.create() + private val selectedMangas = mutableSetOf() - /** - * Relay to notify search query changes. - */ - val searchRelay: BehaviorRelay = BehaviorRelay.create() - - /** - * Relay to notify the library's viewpager for updates. - */ - val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() + private lateinit var adapter: LibraryCategoryAdapter - /** - * Relay to notify the library's viewpager to select all manga - */ - val selectAllRelay: PublishRelay = PublishRelay.create() + private var lastClickPosition = -1 - /** - * Relay to notify the library's viewpager to reotagnize all - */ - val reorganizeRelay: PublishRelay> = PublishRelay.create() + private var lastItemPosition: Int? = null + private var lastItem: IFlexible<*>? = null - /** - * Number of manga per row in grid mode. - */ - var mangaPerRow = 0 + lateinit var presenter: LibraryPresenter private set - /** - * Adapter of the view pager. - */ - private var adapter: LibraryAdapter? = null - - /** - * Navigation view containing filter/sort/display items. - */ - private var navView: LibraryNavigationView? = null - - /** - * Drawer listener to allow swipe only for closing the drawer. - */ - private var drawerListener: DrawerLayout.DrawerListener? = null - - private var tabsVisibilityRelay: BehaviorRelay = BehaviorRelay.create(false) - - private var tabsVisibilitySubscription: Subscription? = null - - private var searchViewSubscription: Subscription? = null + private var observeLater: Boolean = false var snack: Snackbar? = null - private var reorderMenuItem:MenuItem? = null + private var scrollDistance = 0f + private val scrollDistanceTilHidden = 1000.dpToPx - init { - setHasOptionsMenu(true) - retainViewMode = RetainViewMode.RETAIN_DETACH - } + private var textAnim: ViewPropertyAnimator? = null + private var scrollAnim: ViewPropertyAnimator? = null + private var alwaysShowScroller: Boolean = preferences.alwaysShowSeeker().getOrDefault() override fun getTitle(): String? { - return resources?.getString(R.string.label_library) + return view?.context?.getString(R.string.library) + } + + private var scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val order = getCategoryOrder() + if (bottom_sheet.canHide()) { + scrollDistance += abs(dy) + if (scrollDistance > scrollDistanceTilHidden) { + bottom_sheet.hideIfPossible() + scrollDistance = 0f + } + } else scrollDistance = 0f + if (order != null && order != activeCategory && lastItem == null) { + preferences.lastUsedCategory().set(order) + activeCategory = order + if (presenter.categories.size > 1 && dy != 0 && abs(dy) > 75) { + val headerItem = getHeader() ?: return + val view = fast_scroller.getChildAt(0) ?: return + val index = adapter.headerItems.indexOf(headerItem) + textAnim?.cancel() + textAnim = text_view_m.animate().alpha(0f).setDuration(250L).setStartDelay(2000) + textAnim?.start() + + // fastScroll height * indicator position - center text - fastScroll padding + text_view_m.translationY = view.height * + (index.toFloat() / (adapter.headerItems.size + 1)) + - text_view_m.height / 2 + 16.dpToPx + text_view_m.translationX = 45f.dpToPx + text_view_m.alpha = 1f + text_view_m.text = headerItem.category.name + } + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (alwaysShowScroller) return + when (newState) { + RecyclerView.SCROLL_STATE_DRAGGING -> { + scrollAnim?.cancel() + if (fast_scroller.translationX != 0f) { + fast_scroller.animate().setStartDelay(0).setDuration(100).translationX(0f) + .start() + } + } + RecyclerView.SCROLL_STATE_IDLE -> { + scrollAnim = fast_scroller.animate().setStartDelay(1000).setDuration(250) + .translationX(25f.dpToPx) + scrollAnim?.start() + } + } + } } - override fun createPresenter(): LibraryPresenter { - return LibraryPresenter() + private fun hideScroller(duration: Long = 1000) { + if (alwaysShowScroller) return + scrollAnim = + fast_scroller.animate().setStartDelay(duration).setDuration(250).translationX(25f.dpToPx) + scrollAnim?.start() } - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.library_controller, container, false) + private fun setFastScrollBackground() { + val context = activity ?: return + fast_scroller.background = if (!alwaysShowScroller) ContextCompat.getDrawable( + context, R.drawable.fast_scroll_background + ) else null + fast_scroller.textColor = ColorStateList.valueOf( + if (!alwaysShowScroller) Color.WHITE + else context.getResourceColor(android.R.attr.textColorPrimary) + ) + fast_scroller.iconColor = fast_scroller.textColor } override fun onViewCreated(view: View) { super.onViewCreated(view) - - adapter = LibraryAdapter(this) - library_pager.adapter = adapter - library_pager.pageSelections().skip(1).subscribeUntilDestroy { - preferences.lastUsedCategory().set(it) - activeCategory = it + view.applyWindowInsetsForRootController(activity!!.bottom_nav) + if (!::presenter.isInitialized) presenter = LibraryPresenter(this) + if (!alwaysShowScroller) fast_scroller.translationX = 25f.dpToPx + setFastScrollBackground() + + adapter = LibraryCategoryAdapter(this) + adapter.expandItemsAtStartUp() + adapter.isRecursiveCollapse = true + setRecyclerLayout() + recycler.manager.spanSizeLookup = (object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + if (libraryLayout == 0) return 1 + val item = this@LibraryController.adapter.getItem(position) + return if (item is LibraryHeaderItem) recycler.manager.spanCount + else if (item is LibraryItem && item.manga.isBlank()) recycler.manager.spanCount + else 1 + } + }) + recycler.setHasFixedSize(true) + recycler.adapter = adapter + fast_scroller.setupWithRecyclerView(recycler, { position -> + val letter = adapter.getSectionText(position) + if (!singleCategory && + !adapter.isHeader(adapter.getItem(position)) && + position != adapter.itemCount - 1) null + else if (letter != null) FastScrollItemIndicator.Text(letter) + else FastScrollItemIndicator.Icon(R.drawable.ic_star_24dp) + }) + fast_scroller.useDefaultScroller = false + fast_scroller.itemIndicatorSelectedCallbacks += object : + FastScrollerView.ItemIndicatorSelectedCallback { + override fun onItemIndicatorSelected( + indicator: FastScrollItemIndicator, + indicatorCenterY: Int, + itemPosition: Int + ) { + fast_scroller.translationX = 0f + hideScroller(2000) + + textAnim?.cancel() + textAnim = text_view_m.animate().alpha(0f).setDuration(250L).setStartDelay(2000) + textAnim?.start() + + text_view_m.translationY = indicatorCenterY.toFloat() - text_view_m.height / 2 + text_view_m.translationX = 0f + text_view_m.alpha = 1f + text_view_m.text = adapter.onCreateBubbleText(itemPosition) + val appbar = activity?.appbar + + if (singleCategory) { + val order = when (val item = adapter.getItem(itemPosition)) { + is LibraryHeaderItem -> item + is LibraryItem -> item.header + else -> null + }?.category?.order + if (order != null) { + activeCategory = order + preferences.lastUsedCategory().set(order) + } + } + appbar?.y = 0f + recycler.suppressLayout(true) + (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + itemPosition, + if (singleCategory) 0 else (if (itemPosition == 0) 0 else (-40).dpToPx) + ) + recycler.suppressLayout(false) + } + } + recycler.addOnScrollListener(scrollListener) + + val tv = TypedValue() + activity!!.theme.resolveAttribute(R.attr.actionBarTintColor, tv, true) + swipe_refresh.setStyle() + scrollViewWith(recycler, swipeRefreshLayout = swipe_refresh) { insets -> + fast_scroller.updateLayoutParams { + topMargin = insets.systemWindowInsetTop + } } - library_pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageSelected(position: Int) { - enableReorderItems(position) + swipe_refresh.setOnRefreshListener { + swipe_refresh.isRefreshing = false + if (!LibraryUpdateService.isRunning()) { + when { + presenter.allCategories.size <= 1 -> updateLibrary() + preferences.updateOnRefresh().getOrDefault() == -1 -> { + MaterialDialog(activity!!).title(R.string.what_should_update) + .negativeButton(android.R.string.cancel) + .listItemsSingleChoice(items = listOf( + view.context.getString( + R.string.top_category, presenter.allCategories.first().name + ), view.context.getString( + R.string.categories_in_global_update + ) + ), selection = { _, index, _ -> + preferences.updateOnRefresh().set(index) + when (index) { + 0 -> updateLibrary(presenter.allCategories.first()) + else -> updateLibrary() + } + }).positiveButton(R.string.update).show() + } + else -> { + when (preferences.updateOnRefresh().getOrDefault()) { + 0 -> updateLibrary(presenter.allCategories.first()) + else -> updateLibrary() + } + } + } } + } - override fun onPageScrolled( - position: Int, - positionOffset: Float, - positionOffsetPixels: Int - ) { } + if (selectedMangas.isNotEmpty()) { + createActionModeIfNeeded() + } - override fun onPageScrollStateChanged(state: Int) { } - }) + bottom_sheet.onCreate(recycler_layout) - getColumnsPreferenceForCurrentOrientation().asObservable() - .doOnNext { mangaPerRow = it } - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribeUntilDestroy { reattachAdapter() } + bottom_sheet.onGroupClicked = { + when (it) { + FilterBottomSheet.ACTION_REFRESH -> onRefresh() + FilterBottomSheet.ACTION_FILTER -> onFilterChanged() + FilterBottomSheet.ACTION_HIDE_FILTER_TIP -> activity?.toast( + R.string.hide_filters_tip, Toast.LENGTH_LONG + ) + FilterBottomSheet.ACTION_DISPLAY -> DisplayBottomSheet(this).show() + } + } - if (selectedMangas.isNotEmpty()) { - createActionModeIfNeeded() + // pad the recycler if the filter bottom sheet is visible + val height = view.context.resources.getDimensionPixelSize(R.dimen.rounder_radius) + 4.dpToPx + recycler.updatePaddingRelative(bottom = height) + + presenter.onRestore() + if (presenter.libraryItems.isNotEmpty()) { + onNextLibraryUpdate(presenter.libraryItems, true) + } else { + recycler_layout.alpha = 0f + presenter.getLibrary() } } - fun enableReorderItems(category: Category) { - adapter?.categories?.getOrNull(library_pager.currentItem)?.mangaSort = category.mangaSort - enableReorderItems(sortType = category.mangaSort) + private fun getHeader(): LibraryHeaderItem? { + val position = + (recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (position > 0) { + when (val item = adapter.getItem(position)) { + is LibraryHeaderItem -> return item + is LibraryItem -> return item.header + } + } else { + val fPosition = + (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + when (val item = adapter.getItem(fPosition)) { + is LibraryHeaderItem -> return item + is LibraryItem -> return item.header + } + } + return null } - private fun enableReorderItems(position: Int? = null, sortType: Char? = null) { - val pos = position ?: library_pager.currentItem - val orderOfCat = sortType ?: adapter?.categories?.getOrNull(pos)?.mangaSort - if (reorderMenuItem?.isVisible != true) return - val subMenu = reorderMenuItem?.subMenu ?: return - if (orderOfCat != null) { - subMenu.setGroupCheckable(R.id.reorder_group, true, true) - when (orderOfCat) { - 'a', 'b' -> subMenu.findItem(R.id.action_alpha_asc)?.isChecked = true - 'c', 'd' -> subMenu.findItem(R.id.action_update_asc)?.isChecked = true - 'e', 'f' -> subMenu.findItem(R.id.action_unread)?.isChecked = true - 'g', 'h' -> subMenu.findItem(R.id.action_last_read)?.isChecked = true + private fun getCategoryOrder(): Int? { + val position = + (recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + var order = when (val item = adapter.getItem(position)) { + is LibraryHeaderItem -> item.category.order + is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order + else -> null + } + if (order == null) { + val fPosition = + (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + order = when (val item = adapter.getItem(fPosition)) { + is LibraryHeaderItem -> item.category.order + is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order + else -> null } - subMenu.findItem(R.id.action_reverse)?.isVisible = true } - else { - subMenu.findItem(R.id.action_reverse)?.isVisible = false - subMenu.setGroupCheckable(R.id.reorder_group, false, false) + return order + } + + fun updateShowScrollbar(show: Boolean) { + alwaysShowScroller = show + setFastScrollBackground() + if (libraryLayout == 0) reattachAdapter() + scrollAnim?.cancel() + if (show) fast_scroller.translationX = 0f + else hideScroller() + setRecyclerLayout() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.library_list_controller, container, false) + } + + private fun updateLibrary(category: Category? = null) { + val view = view ?: return + LibraryUpdateService.start(view.context, category) + snack = view.snack(R.string.updating_library) { + anchorView = bottom_sheet + } + } + + private fun setRecyclerLayout() { + if (libraryLayout == 0) { + recycler.spanCount = 1 + recycler.updatePaddingRelative( + start = 0, end = 0 + ) + } else { + recycler.columnWidth = when (preferences.gridSize().getOrDefault()) { + 0 -> 1f + 2 -> 1.66f + else -> 1.25f + } + recycler.updatePaddingRelative( + start = (if (alwaysShowScroller) 2 else 5).dpToPx, + end = (if (alwaysShowScroller) 12 else 5).dpToPx + ) } } override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeStarted(handler, type) if (type.isEnter) { - activity?.tabs?.setupWithViewPager(library_pager) - presenter.subscribeLibrary() + view?.applyWindowInsetsForRootController(activity!!.bottom_nav) + presenter.getLibrary() + DownloadService.callListeners() + LibraryUpdateService.setListener(this) + } + if (type == ControllerChangeType.POP_ENTER) bottom_sheet.hideIfPossible() + } + + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + if (view == null) return + if (observeLater && ::presenter.isInitialized) { + presenter.getLibrary() } } + override fun onActivityPaused(activity: Activity) { + super.onActivityPaused(activity) + observeLater = true + if (::presenter.isInitialized) presenter.onDestroy() + } + + override fun onDestroy() { + if (::presenter.isInitialized) presenter.onDestroy() + super.onDestroy() + } + override fun onDestroyView(view: View) { - adapter?.onDestroy() - adapter = null + LibraryUpdateService.removeListener(this) actionMode = null - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null super.onDestroyView(view) } - override fun onDetach(view: View) { + fun onNextLibraryUpdate(mangaMap: List, freshStart: Boolean = false) { + if (view == null) return destroyActionModeIfNeeded() - snack?.dismiss() - snack = null - super.onDetach(view) - } - - override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { - val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView - navView = view - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - - navView?.onGroupClicked = { group, item -> - when (group) { - is LibraryNavigationView.FilterGroup -> onFilterChanged(item) - is LibraryNavigationView.SortGroup -> onSortChanged() - is LibraryNavigationView.DisplayGroup -> reattachAdapter() - is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() + if (mangaMap.isNotEmpty()) { + empty_view?.hide() + } else { + empty_view?.show( + R.drawable.ic_book_black_128dp, + if (bottom_sheet.hasActiveFilters()) R.string.no_matches_for_filters + else R.string.library_is_empty_add_from_browse + ) + } + adapter.setItems(mangaMap) + singleCategory = presenter.categories.size <= 1 + + progress.gone() + if (!freshStart) { + justStarted = false + if (recycler_layout.alpha == 0f) recycler_layout.animate().alpha(1f).setDuration(500) + .start() + } else if (justStarted && freshStart) { + scrollToHeader(activeCategory) + fast_scroller.translationX = 0f + view?.post { + hideScroller(2000) } } - - drawer.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - val statusScrim = view.findViewById(R.id.status_bar_scrim) as View - statusScrim.setOnApplyWindowInsetsListener(HeightTopWindowInsetsListener) - view.doOnApplyWindowInsets { _, insets, _ -> - view.recycler.updatePaddingRelative( - bottom = view.recycler.marginBottom + insets.systemWindowInsetBottom, - top = view.recycler.marginTop + insets.systemWindowInsetTop + adapter.isLongPressDragEnabled = canDrag() + } + + private fun scrollToHeader(pos: Int) { + val headerPosition = adapter.indexOf(pos) + if (headerPosition > -1) { + val appbar = activity?.appbar + recycler.suppressLayout(true) + val appbarOffset = if (appbar?.y ?: 0f > -20) 0 else (appbar?.y?.plus( + view?.rootWindowInsets?.systemWindowInsetTop ?: 0 + ) ?: 0f).roundToInt() + 30.dpToPx + (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + headerPosition, (if (headerPosition == 0) 0 else (-40).dpToPx) + appbarOffset ) + recycler.suppressLayout(false) } - return view } - override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { - navView = null + private fun onRefresh() { + activity?.invalidateOptionsMenu() + presenter.getLibrary() + destroyActionModeIfNeeded() } - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_CENTER - tabMode = TabLayout.MODE_SCROLLABLE + /** + * Called when a filter is changed. + */ + private fun onFilterChanged() { + activity?.invalidateOptionsMenu() + presenter.requestFilterUpdate() + destroyActionModeIfNeeded() + } + + fun reattachAdapter() { + libraryLayout = preferences.libraryLayout().getOrDefault() + setRecyclerLayout() + val position = + (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + recycler.adapter = adapter + + (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(position, 0) + } + + fun search(query: String?): Boolean { + this.query = query ?: "" + adapter.setFilter(query) + adapter.performFilter() + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) { + selectedMangas.clear() + actionMode = null + adapter.mode = SelectableAdapter.Mode.SINGLE + adapter.clearSelection() + adapter.notifyDataSetChanged() + lastClickPosition = -1 + adapter.isLongPressDragEnabled = canDrag() + } + + private fun setSelection(manga: Manga, selected: Boolean) { + val currentMode = adapter.mode + if (selected) { + if (selectedMangas.add(manga)) { + val positions = adapter.allIndexOf(manga) + if (adapter.mode != SelectableAdapter.Mode.MULTI) { + adapter.mode = SelectableAdapter.Mode.MULTI + } + launchUI { + delay(100) + adapter.isLongPressDragEnabled = false + } + positions.forEach { position -> + adapter.addSelection(position) + (recycler.findViewHolderForAdapterPosition(position) as? LibraryHolder)?.toggleActivation() + } + } + } else { + if (selectedMangas.remove(manga)) { + val positions = adapter.allIndexOf(manga) + lastClickPosition = -1 + if (selectedMangas.isEmpty()) { + adapter.mode = SelectableAdapter.Mode.SINGLE + adapter.isLongPressDragEnabled = canDrag() + } + positions.forEach { position -> + adapter.removeSelection(position) + (recycler.findViewHolderForAdapterPosition(position) as? LibraryHolder)?.toggleActivation() + } + } } - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible -> - val tabAnimator = (activity as? MainActivity)?.tabAnimator - if (visible) { - tabAnimator?.expand() + updateHeaders(currentMode != adapter.mode) + } + + private fun updateHeaders(changedMode: Boolean = false) { + val headerPositions = adapter.getHeaderPositions() + headerPositions.forEach { + if (changedMode) { + adapter.notifyItemChanged(it) } else { - tabAnimator?.collapse() + (recycler.findViewHolderForAdapterPosition(it) as? LibraryHeaderItem.Holder)?.setSelection() } } } - override fun cleanupTabs(tabs: TabLayout) { - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null + override fun startReading(position: Int) { + if (adapter.mode == SelectableAdapter.Mode.MULTI) { + toggleSelection(position) + return + } + val manga = (adapter.getItem(position) as? LibraryItem)?.manga ?: return + val activity = activity ?: return + val chapter = presenter.getFirstUnread(manga) ?: return + val intent = ReaderActivity.newIntent(activity, manga, chapter) + destroyActionModeIfNeeded() + startActivity(intent) } - fun onNextLibraryUpdate(categories: List, mangaMap: Map>) { - val view = view ?: return - val adapter = adapter ?: return + private fun toggleSelection(position: Int) { + val item = adapter.getItem(position) as? LibraryItem ?: return + if (item.manga.isBlank()) return + setSelection(item.manga, !adapter.isSelected(position)) + invalidateActionMode() + } - // Show empty view if needed - if (mangaMap.isNotEmpty()) { - empty_view.hide() + override fun canDrag(): Boolean { + val filterOff = + !bottom_sheet.hasActiveFilters() && !preferences.hideCategories().getOrDefault() + return filterOff && adapter.mode != SelectableAdapter.Mode.MULTI + } + + /** + * Called when a manga is clicked. + * + * @param position the position of the element clicked. + * @return true if the item should be selected, false otherwise. + */ + override fun onItemClick(view: View?, position: Int): Boolean { + val item = adapter.getItem(position) as? LibraryItem ?: return false + return if (adapter.mode == SelectableAdapter.Mode.MULTI) { + lastClickPosition = position + toggleSelection(position) + false } else { - empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library) + openManga(item.manga) + false } + } - // Get the current active category. - val activeCat = if (adapter.categories.isNotEmpty()) - library_pager.currentItem - else - activeCategory + private fun openManga(manga: Manga) = router.pushController( + MangaDetailsController( + manga + ).withFadeTransaction() + ) - categories.find { it.id == 0 }?.let { - it.name = resources?.getString( - if (categories.size == 1) R.string.pref_category_library - else R.string.default_columns - ) ?: "Default" + /** + * Called when a manga is long clicked. + * + * @param position the position of the element clicked. + */ + override fun onItemLongClick(position: Int) { + if (adapter.getItem(position) is LibraryHeaderItem) return + createActionModeIfNeeded() + when { + lastClickPosition == -1 -> setSelection(position) + lastClickPosition > position -> for (i in position until lastClickPosition) setSelection( + i + ) + lastClickPosition < position -> for (i in lastClickPosition + 1..position) setSelection( + i + ) + else -> setSelection(position) } - // Set the categories - adapter.categories = categories - - // Restore active category. - library_pager.setCurrentItem(activeCat, false) - - tabsVisibilityRelay.call(categories.size > 1) + lastClickPosition = position + } - // Delay the scroll position to allow the view to be properly measured. - view.post { - if (isAttached) { - activity?.tabs?.setScrollPosition(library_pager.currentItem, 0f, true) + override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + val position = viewHolder?.adapterPosition ?: return + swipe_refresh.isEnabled = actionState != ItemTouchHelper.ACTION_STATE_DRAG + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + activity?.appbar?.y = 0f + if (lastItemPosition != null && position != lastItemPosition && lastItem == adapter.getItem( + position + ) + ) { + // because for whatever reason you can repeatedly tap on a currently dragging manga + adapter.removeSelection(position) + (recycler.findViewHolderForAdapterPosition(position) as? LibraryHolder)?.toggleActivation() + adapter.moveItem(position, lastItemPosition!!) + } else { + lastItem = adapter.getItem(position) + lastItemPosition = position + onItemLongClick(position) } } + } - // Send the manga map to child fragments after the adapter is updated. - libraryMangaRelay.call(LibraryMangaEvent(mangaMap)) + override fun onUpdateManga(manga: LibraryManga) { + if (manga.id == null) adapter.notifyDataSetChanged() + else presenter.updateManga(manga) } - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - private fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) - preferences.portraitColumns() - else - preferences.landscapeColumns() + private fun setSelection(position: Int, selected: Boolean = true) { + val item = adapter.getItem(position) as? LibraryItem ?: return + + setSelection(item.manga, selected) + invalidateActionMode() } - /** - * Called when a filter is changed. - */ - private fun onFilterChanged(item: ExtendedNavigationView.Item) { - if (item is ExtendedNavigationView.Item.MultiStateGroup && - item.resTitle == R.string.action_hide_categories) { - activity?.invalidateOptionsMenu() - presenter.requestFullUpdate() + override fun onItemMove(fromPosition: Int, toPosition: Int) { + // Because padding a recycler causes it to scroll up we have to scroll it back down... wild + if ((adapter.getItem(fromPosition) is LibraryItem && adapter.getItem(fromPosition) is + LibraryItem) || adapter.getItem( + fromPosition + ) == null + ) { + recycler.scrollBy(0, recycler.paddingTop) + } + activity?.appbar?.y = 0f + if (lastItemPosition == toPosition) lastItemPosition = null + else if (lastItemPosition == null) lastItemPosition = fromPosition + } + + override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { + if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition) + val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false + val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem + if (toPosition < 1) return false + return (adapter.getItem(toPosition) !is LibraryHeaderItem) && (newHeader?.category?.id == item.manga.category || !presenter.mangaIsInCategory( + item.manga, newHeader?.category?.id + )) + } + + override fun onItemReleased(position: Int) { + lastItem = null + if (adapter.selectedItemCount > 0) { + lastItemPosition = null return } - presenter.requestFilterUpdate() destroyActionModeIfNeeded() - activity?.invalidateOptionsMenu() + // if nothing moved + if (lastItemPosition == null) return + val item = adapter.getItem(position) as? LibraryItem ?: return + val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem + val libraryItems = adapter.getSectionItems(adapter.getSectionHeader(position)) + .filterIsInstance() + val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id } + if (newHeader?.category?.id == item.manga.category) { + presenter.rearrangeCategory(item.manga.category, mangaIds) + } else { + if (presenter.mangaIsInCategory(item.manga, newHeader?.category?.id)) { + adapter.moveItem(position, lastItemPosition!!) + snack = view?.snack(R.string.already_in_category) { + anchorView = bottom_sheet + } + return + } + if (newHeader?.category != null) moveMangaToCategory( + item.manga, newHeader.category, mangaIds + ) + } + lastItemPosition = null } - private fun onDownloadBadgeChanged() { - presenter.requestDownloadBadgesUpdate() + private fun moveMangaToCategory( + manga: LibraryManga, + category: Category?, + mangaIds: List + ) { + if (category?.id == null) return + val oldCatId = manga.category + presenter.moveMangaToCategory(manga, category.id, mangaIds) + snack?.dismiss() + snack = view?.snack( + resources!!.getString(R.string.moved_to_, category.name) + ) { + anchorView = bottom_sheet + setAction(R.string.undo) { + manga.category = category.id!! + presenter.moveMangaToCategory(manga, oldCatId, mangaIds) + } + } } - /** - * Called when the sorting mode is changed. - */ - fun onSortChanged() { - activity?.invalidateOptionsMenu() - presenter.requestSortUpdate() + override fun updateCategory(catId: Int): Boolean { + val category = (adapter.getItem(catId) as? LibraryHeaderItem)?.category ?: return false + val inQueue = LibraryUpdateService.categoryInQueue(category.id) + snack?.dismiss() + snack = view?.snack( + resources!!.getString( + when { + inQueue -> R.string._already_in_queue + LibraryUpdateService.isRunning() -> R.string.adding_category_to_queue + else -> R.string.updating_ + }, category.name + ), Snackbar.LENGTH_LONG + ) { + anchorView = bottom_sheet + } + if (!inQueue) LibraryUpdateService.start(view!!.context, category) + return true } - /** - * Reattaches the adapter to the view pager to recreate fragments - */ - private fun reattachAdapter() { - val adapter = adapter ?: return + override fun sortCategory(catId: Int, sortBy: Int) { + presenter.sortCategory(catId, sortBy) + } - val position = library_pager.currentItem + override fun selectAll(position: Int) { + val header = adapter.getSectionHeader(position) ?: return + val items = adapter.getSectionItemPositions(header) + val allSelected = allSelected(position) + for (i in items) setSelection(i, !allSelected) + } - adapter.recycle = false - library_pager.adapter = adapter - library_pager.currentItem = position - adapter.recycle = true + override fun allSelected(position: Int): Boolean { + val header = adapter.getSectionHeader(position) ?: return false + val items = adapter.getSectionItemPositions(header) + return items.all { adapter.isSelected(it) } } - /** - * Creates the action mode if it's not created already. - */ - fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - val view = activity?.window?.currentFocus ?: return - val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? - InputMethodManager ?: return - imm.hideSoftInputFromWindow(view.windowToken, 0) + override fun showSheet() { + if (bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN) bottom_sheet.sheetBehavior?.state = + BottomSheetBehavior.STATE_COLLAPSED + else bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun toggleSheet() { + when { + bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN -> bottom_sheet.sheetBehavior?.state = + BottomSheetBehavior.STATE_COLLAPSED + bottom_sheet.sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED -> bottom_sheet.sheetBehavior?.state = + BottomSheetBehavior.STATE_EXPANDED + bottom_sheet.sheetBehavior?.isHideable == true -> bottom_sheet.sheetBehavior?.state = + BottomSheetBehavior.STATE_HIDDEN + else -> bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED } } - /** - * Destroys the action mode. - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() + override fun handleSheetBack(): Boolean { + val sheetBehavior = BottomSheetBehavior.from(bottom_sheet) + if (sheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED && sheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + sheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + return true + } + return false } + //region Toolbar options methods override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.library, menu) - val reorganizeItem = menu.findItem(R.id.action_reorganize) - reorganizeItem.isVisible = - preferences.librarySortingMode().getOrDefault() == LibrarySort.DRAG_AND_DROP && - !preferences.hideCategories().getOrDefault() - reorderMenuItem = reorganizeItem - enableReorderItems() - val searchItem = menu.findItem(R.id.action_search) val searchView = searchItem.actionView as SearchView - searchView.queryHint = resources?.getString(R.string.search_hint) + searchView.queryHint = resources?.getString(R.string.library_search_hint) searchItem.collapseActionView() - if (!query.isEmpty()) { + if (query.isNotEmpty()) { searchItem.expandActionView() searchView.setQuery(query, true) searchView.clearFocus() } - // Mutate the filter icon because it needs to be tinted and the resource is shared. - menu.findItem(R.id.action_filter).icon.mutate() - - searchViewSubscription?.unsubscribe() - searchViewSubscription = searchView.queryTextChanges() - // Ignore events if this controller isn't at the top - .filter { router.backstack.lastOrNull()?.controller() == this } - .subscribeUntilDestroy { - query = it.toString() - searchRelay.call(query) - } - + setOnQueryTextChangeListener(searchView) { search(it) } searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() }) } - fun search(query:String) { - this.query = query - } - - override fun onPrepareOptionsMenu(menu: Menu) { - val navView = navView ?: return - - val filterItem = menu.findItem(R.id.action_filter) - - // Tint icon if there's a filter active - val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE - DrawableCompat.setTint(filterItem.icon, filterColor) - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_search -> expandActionViewFromInteraction = true - R.id.action_filter -> { - navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) } - } - R.id.action_edit_categories -> { - router.pushController(CategoryController().withFadeTransaction()) - } - R.id.action_source_migration -> { - router.pushController(MigrationController().withFadeTransaction()) - } - R.id.action_alpha_asc -> reOrder(0) - R.id.action_update_asc -> reOrder(1) - R.id.action_unread -> reOrder(2) - R.id.action_last_read -> reOrder(3) - R.id.action_reverse -> reOrder(-1) + R.id.action_library_display -> DisplayBottomSheet(this).show() else -> return super.onOptionsItemSelected(item) } - return true } + //endregion - private fun reOrder(type: Int) { - val modType = if (type == -1) { - val t = (adapter?.categories?.getOrNull(library_pager.currentItem)?.mangaSort - ?.minus('a') ?: 0) + 1 - if (t % 2 != 0) t + 1 - else t - 1 - } - else 2 * type + 1 - adapter?.categories?.getOrNull(library_pager.currentItem)?.id?.let { - reorganizeRelay.call(it to modType) - onSortChanged() + //region Action Mode Methods + /** + * Creates the action mode if it's not created already. + */ + private fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + val view = activity?.window?.currentFocus ?: return + val imm = + activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + ?: return + imm.hideSoftInputFromWindow(view.windowToken, 0) } } + /** + * Destroys the action mode. + */ + private fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + /** * Invalidates the action mode, forcing it to refresh its content. */ - fun invalidateActionMode() { + private fun invalidateActionMode() { actionMode?.invalidate() } @@ -526,71 +913,25 @@ class LibraryController( override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { val count = selectedMangas.size - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - menu.findItem(R.id.action_hide_title)?.isVisible = - !preferences.libraryAsList().getOrDefault() - if (!preferences.libraryAsList().getOrDefault()) { - val showAll = (selectedMangas.all { (it as? LibraryManga)?.hide_title == true }) - menu.findItem(R.id.action_hide_title)?.title = activity?.getString( - if (showAll) R.string.action_show_title else R.string.action_hide_title - ) - } - if (preferences.librarySortingMode().getOrDefault() == LibrarySort.DRAG_AND_DROP) { - val catId = (selectedMangas.first() as? LibraryManga)?.category - val sameCat = (adapter?.categories?.getOrNull(library_pager.currentItem)?.id - == catId) && selectedMangas.all { (it as? LibraryManga)?.category == catId } - menu.findItem(R.id.action_move_manga).isVisible = sameCat - } - else menu.findItem(R.id.action_move_manga).isVisible = false - } + // Destroy action mode if there are no items selected. + if (count == 0) destroyActionModeIfNeeded() + else mode.title = resources?.getString(R.string.selected_, count) return false } + //endregion override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { when (item.itemId) { R.id.action_move_to_category -> showChangeMangaCategoriesDialog() R.id.action_delete -> { - MaterialDialog(activity!!) - .message(R.string.confirm_manga_deletion) - .positiveButton(R.string.action_remove) { + MaterialDialog(activity!!).message(R.string.remove_from_library_question) + .positiveButton(R.string.remove) { deleteMangasFromLibrary() - } - .negativeButton(android.R.string.no) - .show() - } - R.id.action_select_all -> { - adapter?.categories?.getOrNull(library_pager.currentItem)?.id?.let { - selectAllRelay.call(it) - } + }.negativeButton(android.R.string.no).show() } R.id.action_migrate -> { - router.pushController( - if (preferences.skipPreMigration().getOrDefault()) { - MigrationListController.create( - MigrationProcedureConfig( - selectedMangas.mapNotNull { it.id },null) - ) - } - else { - PreMigrationController.create( selectedMangas.mapNotNull { it.id } ) - } - .withFadeTransaction()) - destroyActionModeIfNeeded() - } - R.id.action_hide_title -> { - val showAll = (selectedMangas.filter { (it as? LibraryManga)?.hide_title == true } - ).size == selectedMangas.size - presenter.hideShowTitle(selectedMangas.toList(), !showAll) - destroyActionModeIfNeeded() - } - R.id.action_to_top, R.id.action_to_bottom -> { - adapter?.categories?.getOrNull(library_pager.currentItem)?.id?.let { - reorganizeRelay.call(it to if (item.itemId == R.id.action_to_top) -1 else -2) - } + val skipPre = preferences.skipPreMigration().getOrDefault() + PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id }) destroyActionModeIfNeeded() } else -> return false @@ -598,93 +939,24 @@ class LibraryController( return true } - override fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean): Manga? { - if (manga.id != prevManga.id) { - presenter.migrateManga(prevManga, manga, replace = replace) - } - val nextManga = migratingMangas.firstOrNull() ?: return null - migratingMangas.remove(nextManga) - return nextManga - } - - override fun onDestroyActionMode(mode: ActionMode?) { - // Clear all the manga selections and notify child views. - selectedMangas.clear() - selectionRelay.call(LibrarySelectionEvent.Cleared()) - actionMode = null - } - - fun openManga(manga: Manga, startY: Float?) { - // Notify the presenter a manga is being opened. - presenter.onOpenManga() - - router.pushController(MangaController(manga, startY).withFadeTransaction()) - } - - /** - * Sets the selection for a given manga. - * - * @param manga the manga whose selection has changed. - * @param selected whether it's now selected or not. - */ - fun setSelection(manga: Manga, selected: Boolean) { - if (selected) { - if (selectedMangas.add(manga)) { - selectionRelay.call(LibrarySelectionEvent.Selected(manga)) - } - } else { - if (selectedMangas.remove(manga)) { - selectionRelay.call(LibrarySelectionEvent.Unselected(manga)) - } - } - } - - /** - * Move the selected manga to a list of categories. - */ - private fun showChangeMangaCategoriesDialog() { - // Create a copy of selected manga - val mangas = selectedMangas.toList() - - // Hide the default category because it has a different behavior than the ones from db. - val categories = presenter.allCategories.filter { it.id != 0 } - - // Get indexes of the common categories to preselect. - val commonCategoriesIndexes = presenter.getCommonCategories(mangas) - .map { categories.indexOf(it) } - .toTypedArray() - - ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) - .showDialog(router) - } - - fun lockFilterBar(lock: Boolean) { - val drawer = (navView?.parent as? DrawerLayout) ?: return - if (lock) { - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - drawer.closeDrawers() - } else { - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - drawer.visible() - } - } - private fun deleteMangasFromLibrary() { val mangas = selectedMangas.toList() presenter.removeMangaFromLibrary(mangas) destroyActionModeIfNeeded() snack?.dismiss() - snack = view?.snack(activity?.getString(R.string.manga_removed_library) ?: "", Snackbar.LENGTH_INDEFINITE) { + snack = view?.snack( + activity?.getString(R.string.removed_from_library) ?: "", Snackbar.LENGTH_INDEFINITE + ) { + anchorView = bottom_sheet var undoing = false - setAction(R.string.action_undo) { - presenter.addMangas(mangas) + setAction(R.string.undo) { + presenter.reAddMangas(mangas) undoing = true } addCallback(object : BaseTransientBottomBar.BaseCallback() { override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { super.onDismissed(transientBottomBar, event) - if (!undoing) - presenter.confirmDeletion(mangas) + if (!undoing) presenter.confirmDeletion(mangas) } }) } @@ -695,16 +967,23 @@ class LibraryController( presenter.moveMangasToCategories(categories, mangas) destroyActionModeIfNeeded() } -} -object HeightTopWindowInsetsListener : View.OnApplyWindowInsetsListener { - override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { - val topInset = insets.systemWindowInsetTop - v.setPadding(0,topInset,0,0) - if (v.layoutParams.height != topInset) { - v.layoutParams.height = topInset - v.requestLayout() - } - return insets + /** + * Move the selected manga to a list of categories. + */ + private fun showChangeMangaCategoriesDialog() { + // Create a copy of selected manga + val mangas = selectedMangas.toList() + + // Hide the default category because it has a different behavior than the ones from db. + val categories = presenter.allCategories.filter { it.id != 0 } + + // Get indexes of the common categories to preselect. + val commonCategoriesIndexes = + presenter.getCommonCategories(mangas).map { categories.indexOf(it) }.toTypedArray() + + ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes).showDialog( + router + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index 6d576fdf00..8a15ff00c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -1,15 +1,16 @@ package eu.kanade.tachiyomi.ui.library +import android.view.Gravity import android.view.View -import androidx.recyclerview.widget.RecyclerView +import android.widget.FrameLayout import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.signature.ObjectKey -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.util.view.gone import kotlinx.android.synthetic.main.catalogue_grid_item.* +import kotlinx.android.synthetic.main.unread_download_badge.* /** * Class used to hold the displayed data of a manga in the library, like the cover or the title. @@ -21,11 +22,29 @@ import kotlinx.android.synthetic.main.catalogue_grid_item.* * @constructor creates a new library holder. */ class LibraryGridHolder( - private val view: View, - adapter: FlexibleAdapter> - + private val view: View, + adapter: LibraryCategoryAdapter, + var width: Int, + compact: Boolean, + private var fixedSize: Boolean ) : LibraryHolder(view, adapter) { + init { + play_layout.setOnClickListener { playButtonClicked() } + if (compact) { + text_layout.gone() + } else { + compact_title.gone() + gradient.gone() + val playLayout = play_layout.layoutParams as FrameLayout.LayoutParams + val buttonLayout = play_button.layoutParams as FrameLayout.LayoutParams + playLayout.gravity = Gravity.BOTTOM or Gravity.END + buttonLayout.gravity = Gravity.BOTTOM or Gravity.END + play_layout.layoutParams = playLayout + play_button.layoutParams = buttonLayout + } + } + /** * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. @@ -33,34 +52,56 @@ class LibraryGridHolder( * @param item the manga item to bind. */ override fun onSetValues(item: LibraryItem) { - // Update the title of the manga. - with(title) { - visibility = if (item.manga.hide_title) View.GONE else View.VISIBLE - text = item.manga.currentTitle() - } - gradient.visibility = if (item.manga.hide_title) View.GONE else View.VISIBLE + // Update the title and subtitle of the manga. + title.text = item.manga.title + subtitle.text = item.manga.author?.trim() - // Update the unread count and its visibility. - with(unread_text) { - visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE - text = item.manga.unread.toString() - } - // Update the download count and its visibility. - with(download_text) { - visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE - text = item.downloadCount.toString() - } - //set local visibility if its local manga - local_text.visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE + compact_title.text = title.text + + setUnreadBadge(badge_view, item) + play_layout.visibility = if (item.manga.unread > 0 && item.unreadType > 0) + View.VISIBLE else View.GONE // Update the cover. - GlideApp.with(view.context).clear(thumbnail) - GlideApp.with(view.context) - .load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(item.manga.id!!).toString())) - .centerCrop() - .into(thumbnail) + if (item.manga.thumbnail_url == null) GlideApp.with(view.context).clear(cover_thumbnail) + else { + val id = item.manga.id ?: return + if (cover_thumbnail.height == 0) { + val oldPos = adapterPosition + adapter.recyclerView.post { + if (oldPos == adapterPosition) + setCover(item.manga, id) + } + } else setCover(item.manga, id) + } } + private fun setCover(manga: Manga, id: Long) { + GlideApp.with(adapter.recyclerView.context).load(manga) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString())) + .apply { + if (fixedSize) centerCrop() + else override(cover_thumbnail.maxHeight) + } + .into(cover_thumbnail) + } + + private fun playButtonClicked() { + adapter.libraryListener.startReading(adapterPosition) + } + + override fun onActionStateChanged(position: Int, actionState: Int) { + super.onActionStateChanged(position, actionState) + if (actionState == 2) { + card.isDragged = true + badge_view.isDragged = true + } + } + + override fun onItemReleased(position: Int) { + super.onItemReleased(position) + card.isDragged = false + badge_view.isDragged = false + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt new file mode 100644 index 0000000000..8d985b2440 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt @@ -0,0 +1,273 @@ +package eu.kanade.tachiyomi.ui.library + +import android.graphics.drawable.Drawable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.PopupMenu +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.f2prateek.rx.preferences.Preference +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.util.system.dpToPx +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.updateLayoutParams +import eu.kanade.tachiyomi.util.view.visInvisIf +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.android.synthetic.main.library_category_header_item.view.* + +class LibraryHeaderItem( + private val categoryF: (Int) -> Category, + private val catId: Int, + private val showFastScroll: Preference +) : + AbstractHeaderItem() { + + override fun getLayoutRes(): Int { + return R.layout.library_category_header_item + } + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): Holder { + return Holder(view, adapter as LibraryCategoryAdapter, showFastScroll.getOrDefault()) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: Holder, + position: Int, + payloads: MutableList? + ) { + holder.bind(categoryF(catId)) + } + + val category: Category + get() = categoryF(catId) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is LibraryHeaderItem) { + return category.id == other.category.id + } + return false + } + + override fun isDraggable(): Boolean { + return false + } + + override fun isSelectable(): Boolean { + return false + } + + override fun hashCode(): Int { + return -(category.id!!) + } + + class Holder(val view: View, private val adapter: LibraryCategoryAdapter, padEnd: Boolean) : + FlexibleViewHolder(view, adapter, true) { + + private val sectionText: TextView = view.findViewById(R.id.category_title) + private val sortText: TextView = view.findViewById(R.id.category_sort) + private val updateButton: ImageView = view.findViewById(R.id.update_button) + private val checkboxImage: ImageView = view.findViewById(R.id.checkbox) + private val catProgress: ProgressBar = view.findViewById(R.id.cat_progress) + + init { + sortText.updateLayoutParams { + marginEnd = (if (padEnd && adapter.recyclerView.paddingEnd == 0) 12 else 2).dpToPx + } + updateButton.setOnClickListener { addCategoryToUpdate() } + sortText.setOnClickListener { it.post { showCatSortOptions() } } + checkboxImage.setOnClickListener { selectAll() } + updateButton.drawable.mutate() + } + + fun bind(category: Category) { + sectionText.updateLayoutParams { + topMargin = (if (category.isFirst == true) 2 else 44).dpToPx + } + + if (category.isFirst == true && category.isLast == true) sectionText.text = "" + else sectionText.text = category.name + sortText.text = itemView.context.getString(R.string.sort_by_, + itemView.context.getString( + when (category.sortingMode()) { + LibrarySort.LATEST_CHAPTER -> R.string.latest_chapter + LibrarySort.DRAG_AND_DROP -> + if (category.id == -1) R.string.category + else R.string.drag_and_drop + LibrarySort.TOTAL -> R.string.total_chapters + LibrarySort.UNREAD -> R.string.unread + LibrarySort.LAST_READ -> R.string.last_read + LibrarySort.ALPHA -> R.string.title + LibrarySort.DATE_ADDED -> R.string.date_added + else -> R.string.drag_and_drop + } + )) + + when { + adapter.mode == SelectableAdapter.Mode.MULTI -> { + checkboxImage.visible() + updateButton.gone() + catProgress.gone() + setSelection() + } + category.id == -1 -> { + checkboxImage.gone() + updateButton.gone() + } + LibraryUpdateService.categoryInQueue(category.id) -> { + checkboxImage.gone() + catProgress.visible() + updateButton.invisible() + } + else -> { + catProgress.gone() + checkboxImage.gone() + updateButton.visInvisIf(!(category.isFirst == true && category.isLast == true)) + } + } + } + + private fun addCategoryToUpdate() { + if (adapter.libraryListener.updateCategory(adapterPosition)) { + catProgress.visible() + updateButton.invisible() + } + } + private fun showCatSortOptions() { + val category = + (adapter.getItem(adapterPosition) as? LibraryHeaderItem)?.category ?: return + // Create a PopupMenu, giving it the clicked view for an anchor + val popup = PopupMenu(itemView.context, view.category_sort) + + // Inflate our menu resource into the PopupMenu's Menu + popup.menuInflater.inflate( + if (category.id == -1) R.menu.main_sort + else R.menu.cat_sort, popup.menu) + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener { menuItem -> + onCatSortClicked(category, menuItem.itemId) + true + } + + val sortingMode = category.sortingMode() + val currentItem = if (sortingMode == null) null + else popup.menu.findItem( + when (sortingMode) { + LibrarySort.DRAG_AND_DROP -> R.id.action_drag_and_drop + LibrarySort.TOTAL -> R.id.action_total_chaps + LibrarySort.LAST_READ -> R.id.action_last_read + LibrarySort.UNREAD -> R.id.action_unread + LibrarySort.LATEST_CHAPTER -> R.id.action_update + LibrarySort.DATE_ADDED -> R.id.action_date_added + else -> R.id.action_alpha + } + ) + + if (category.id == -1) + popup.menu.findItem(R.id.action_drag_and_drop).title = contentView.context.getString( + R.string.category + ) + + if (sortingMode != null && popup.menu is MenuBuilder) { + val m = popup.menu as MenuBuilder + m.setOptionalIconsVisible(true) + } + + val isAscending = category.isAscending() + + currentItem?.icon = tintVector( + when { + sortingMode == LibrarySort.DRAG_AND_DROP -> R.drawable.ic_check_white_24dp + if (sortingMode == LibrarySort.DATE_ADDED || + sortingMode == LibrarySort.LATEST_CHAPTER || + sortingMode == LibrarySort.LAST_READ) !isAscending else isAscending -> + R.drawable.ic_arrow_down_white_24dp + else -> R.drawable.ic_arrow_up_white_24dp + } + ) + val s = SpannableString(currentItem?.title ?: "") + s.setSpan(ForegroundColorSpan(itemView.context.getResourceColor(android.R.attr.colorAccent)), 0, s.length, 0) + currentItem?.title = s + + // Finally show the PopupMenu + popup.show() + } + + private fun tintVector(resId: Int): Drawable? { + return ContextCompat.getDrawable(itemView.context, resId)?.mutate()?.apply { + setTint(itemView.context.getResourceColor(android.R.attr.colorAccent)) + } + } + + private fun onCatSortClicked(category: Category, menuId: Int?) { + val modType = if (menuId == null) { + val t = (category.mangaSort?.minus('a') ?: 0) + 1 + if (t % 2 != 0) t + 1 + else t - 1 + } else { + val order = when (menuId) { + R.id.action_drag_and_drop -> { + adapter.libraryListener.sortCategory(category.id!!, 'D' - 'a' + 1) + return + } + R.id.action_date_added -> 5 + R.id.action_total_chaps -> 4 + R.id.action_last_read -> 3 + R.id.action_unread -> 2 + R.id.action_update -> 1 + else -> 0 + } + if (order == category.catSortingMode()) { + onCatSortClicked(category, null) + return + } + (2 * order + 1) + } + adapter.libraryListener.sortCategory(category.id!!, modType) + } + + private fun selectAll() { + adapter.libraryListener.selectAll(adapterPosition) + } + + fun setSelection() { + val allSelected = adapter.libraryListener.allSelected(adapterPosition) + val drawable = + ContextCompat.getDrawable(contentView.context, + if (allSelected) R.drawable.ic_check_circle_white_24dp else + R.drawable.ic_radio_button_unchecked_white_24dp) + val tintedDrawable = drawable?.mutate() + tintedDrawable?.setTint(ContextCompat.getColor(contentView.context, + if (allSelected) R.color.colorAccent + else R.color.gray_button)) + checkboxImage.setImageDrawable(tintedDrawable) + } + + override fun onLongClick(view: View?): Boolean { + super.onLongClick(view) + return false + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index 1cc6144314..22011109a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -1,10 +1,8 @@ package eu.kanade.tachiyomi.ui.library import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.items.IFlexible /** * Generic class used to hold the displayed data of a manga in the library. @@ -14,8 +12,8 @@ import eu.davidea.flexibleadapter.items.IFlexible */ abstract class LibraryHolder( - view: View, - val adapter: FlexibleAdapter> + view: View, + val adapter: LibraryCategoryAdapter ) : BaseFlexibleViewHolder(view, adapter) { /** @@ -26,6 +24,21 @@ abstract class LibraryHolder( */ abstract fun onSetValues(item: LibraryItem) + fun setUnreadBadge(badge: LibraryBadge, item: LibraryItem) { + badge.setUnreadDownload( + when { + item.chapterCount > -1 -> item.chapterCount + item.unreadType == 2 -> item.manga.unread + item.unreadType == 1 -> if (item.manga.unread > 0) -1 else -2 + else -> -2 + }, + when { + item.downloadCount == -1 -> -1 + item.manga.source == LocalSource.ID -> -2 + else -> item.downloadCount + }, + item.chapterCount > -1) + } /** * Called when an item is released. @@ -34,7 +47,13 @@ abstract class LibraryHolder( */ override fun onItemReleased(position: Int) { super.onItemReleased(position) - (adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position) + (adapter as? LibraryCategoryAdapter)?.libraryListener?.onItemReleased(position) } + override fun onLongClick(view: View?): Boolean { + return if (adapter.isLongPressDragEnabled) { + super.onLongClick(view) + false + } else super.onLongClick(view) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 062fb173eb..36c00c8710 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -3,29 +3,42 @@ package eu.kanade.tachiyomi.ui.library import android.annotation.SuppressLint import android.view.Gravity import android.view.View -import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup import android.widget.FrameLayout +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.f2prateek.rx.preferences.Preference import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.IFilterable import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import uy.kohesive.injekt.injectLazy -class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference) : - AbstractFlexibleItem(), IFilterable { +class LibraryItem( + val manga: LibraryManga, + private val libraryLayout: Preference, + private val fixedSize: Preference, + private val showFastScroll: Preference, + header: LibraryHeaderItem? +) : + AbstractSectionableItem(header), IFilterable { var downloadCount = -1 + var unreadType = 2 + var chapterCount = -1 override fun getLayoutRes(): Int { - return if (libraryAsList.getOrDefault()) + return if (libraryLayout.getOrDefault() == 0 || manga.isBlank()) R.layout.catalogue_list_item else R.layout.catalogue_grid_item @@ -34,23 +47,64 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LibraryHolder { val parent = adapter.recyclerView return if (parent is AutofitRecyclerView) { - view.apply { - val coverHeight = parent.itemWidth / 3 * 4 - card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) - gradient.layoutParams = FrameLayout.LayoutParams( - MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) + val libraryLayout = libraryLayout.getOrDefault() + val isFixedSize = fixedSize.getOrDefault() + if (libraryLayout == 0 || manga.isBlank()) { + LibraryListHolder(view, adapter as LibraryCategoryAdapter, showFastScroll.getOrDefault()) + } else { + view.apply { + val coverHeight = (parent.itemWidth / 3f * 4f).toInt() + if (libraryLayout == 1) { + gradient.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + (coverHeight * 0.66f).toInt(), + Gravity.BOTTOM) + card.updateLayoutParams { + bottomMargin = 6.dpToPx + } + } else if (libraryLayout == 2) { + constraint_layout.background = ContextCompat.getDrawable( + context, R.drawable.library_item_selector + ) + } + if (isFixedSize) { + constraint_layout.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + cover_thumbnail.maxHeight = Int.MAX_VALUE + cover_thumbnail.minimumHeight = 0 + constraint_layout.minHeight = 0 + cover_thumbnail.scaleType = ImageView.ScaleType.CENTER_CROP + cover_thumbnail.adjustViewBounds = false + cover_thumbnail.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + (parent.itemWidth / 3f * 3.7f).toInt() + ) + } else { + constraint_layout.minHeight = coverHeight + cover_thumbnail.minimumHeight = (parent.itemWidth / 3f * 3.6f).toInt() + cover_thumbnail.maxHeight = (parent.itemWidth / 3f * 6f).toInt() + } + } + LibraryGridHolder( + view, + adapter as LibraryCategoryAdapter, + parent.itemWidth, + libraryLayout == 1, + isFixedSize + ) } - LibraryGridHolder(view, adapter) } else { - LibraryListHolder(view, adapter) + LibraryListHolder(view, adapter as LibraryCategoryAdapter, showFastScroll.getOrDefault()) } } - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: LibraryHolder, - position: Int, - payloads: MutableList?) { - + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: LibraryHolder, + position: Int, + payloads: MutableList? + ) { holder.onSetValues(this) } @@ -58,7 +112,15 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference * Returns true if this item is draggable. */ override fun isDraggable(): Boolean { - return true + return !manga.isBlank() + } + + override fun isEnabled(): Boolean { + return !manga.isBlank() + } + + override fun isSelectable(): Boolean { + return !manga.isBlank() } /** @@ -68,19 +130,19 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference * @return true if the manga should be included, false otherwise. */ override fun filter(constraint: String): Boolean { + if (manga.isBlank()) + return constraint.isEmpty() val sourceManager by injectLazy() val sourceName = if (manga.source == 0L) "Local" else sourceManager.getOrStub(manga.source).name - return manga.currentTitle().contains(constraint, true) || - manga.originalTitle().contains(constraint, true) || - (manga.currentAuthor()?.contains(constraint, true) ?: false) || - (manga.currentArtist()?.contains(constraint, true) ?: false) || + return manga.title.contains(constraint, true) || + (manga.author?.contains(constraint, true) ?: false) || + (manga.artist?.contains(constraint, true) ?: false) || sourceName.contains(constraint, true) || if (constraint.contains(",")) { - val genres = manga.currentGenres()?.split(", ") + val genres = manga.genre?.split(", ") constraint.split(",").all { containsGenre(it.trim(), genres) } - } - else containsGenre(constraint, manga.currentGenres()?.split(", ")) + } else containsGenre(constraint, manga.genre?.split(", ")) } @SuppressLint("DefaultLocale") @@ -89,7 +151,7 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference return if (tag.startsWith("-")) genres?.find { it.trim().toLowerCase() == tag.substringAfter("-").toLowerCase() - } == null + } == null else genres?.find { it.trim().toLowerCase() == tag.toLowerCase() } != null @@ -97,12 +159,12 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference override fun equals(other: Any?): Boolean { if (other is LibraryItem) { - return manga.id == other.manga.id + return manga.id == other.manga.id && manga.category == other.manga.category } return false } override fun hashCode(): Int { - return manga.id!!.hashCode() + return (manga.id!! + (manga.category shl 50).toLong()).hashCode() // !!.hashCode() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index ab3a2ca3d8..a09885e373 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -1,15 +1,20 @@ package eu.kanade.tachiyomi.ui.library import android.view.View +import android.view.ViewGroup +import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.source.LocalSource -import kotlinx.android.synthetic.main.catalogue_list_item.* -import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.signature.ObjectKey -import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.android.synthetic.main.catalogue_list_item.* +import kotlinx.android.synthetic.main.catalogue_list_item.view.* +import kotlinx.android.synthetic.main.unread_download_badge.* /** * Class used to hold the displayed data of a manga in the library, like the cover or the title. @@ -22,10 +27,17 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl */ class LibraryListHolder( - private val view: View, - adapter: FlexibleAdapter> + private val view: View, + adapter: LibraryCategoryAdapter, + padEnd: Boolean ) : LibraryHolder(view, adapter) { + init { + badge_view?.updateLayoutParams { + marginEnd = (if (padEnd) 22 else 12).dpToPx + } + } + /** * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. @@ -33,38 +45,56 @@ class LibraryListHolder( * @param item the manga item to bind. */ override fun onSetValues(item: LibraryItem) { - // Update the title of the manga. - title.text = item.manga.currentTitle() - // Update the unread count and its visibility. - with(unread_text) { - visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE - text = item.manga.unread.toString() + if (item.manga.isBlank()) { + title.text = itemView.context.getString(R.string.category_is_empty) + title.textAlignment = View.TEXT_ALIGNMENT_CENTER + card.gone() + badge_view.gone() + return } - // Update the download count and its visibility. - with(download_text) { - visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE - text = "${item.downloadCount}" - } - //show local text badge if local manga - local_text.visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE + card.visible() + title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START - // Create thumbnail onclick to simulate long click - thumbnail.setOnClickListener { - // Simulate long click on this view to enter selection mode - onLongClick(itemView) - } + // Update the title of the manga. + title.text = item.manga.title + setUnreadBadge(badge_view, item) + + subtitle.text = item.manga.author?.trim() + subtitle.visibility = if (!item.manga.author.isNullOrBlank()) View.VISIBLE + else View.GONE + + play_layout.visibility = if (item.manga.unread > 0 && item.unreadType > 0) + View.VISIBLE else View.GONE + play_layout.setOnClickListener { playButtonClicked() } // Update the cover. - GlideApp.with(itemView.context).clear(thumbnail) - GlideApp.with(itemView.context) - .load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(item.manga.id!!).toString())) - .centerCrop() - .circleCrop() - .dontAnimate() - .into(thumbnail) + if (item.manga.thumbnail_url == null) Glide.with(view.context).clear(cover_thumbnail) + else { + val id = item.manga.id ?: return + val height = itemView.context.resources.getDimensionPixelSize(R.dimen + .material_component_lists_single_line_with_avatar_height) + GlideApp.with(view.context).load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString())) + .override(height) + .into(cover_thumbnail) + } + } + + private fun playButtonClicked() { + adapter.libraryListener.startReading(adapterPosition) } + override fun onActionStateChanged(position: Int, actionState: Int) { + super.onActionStateChanged(position, actionState) + if (actionState == 2) { + view.card.isDragged = true + } + } + + override fun onItemReleased(position: Int) { + super.onItemReleased(position) + view.card.isDragged = false + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt deleted file mode 100644 index e5dffe3086..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import eu.kanade.tachiyomi.data.database.models.Category - -class LibraryMangaEvent(val mangas: Map>) { - - fun getMangaForCategory(category: Category): List? { - return mangas[category.id] - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt deleted file mode 100644 index cf766d89a9..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt +++ /dev/null @@ -1,276 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.content.Context -import android.util.AttributeSet -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.widget.ExtendedNavigationView -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_EXCLUDE -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_INCLUDE -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy - -/** - * The navigation view shown in a drawer with the different options to show the library. - */ -class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) - : ExtendedNavigationView(context, attrs) { - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * List of groups shown in the view. - */ - private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup()) - - /** - * Adapter instance. - */ - private val adapter = Adapter(groups.map { it.createItems() }.flatten()) - - /** - * Click listener to notify the parent fragment when an item from a group is clicked. - */ - var onGroupClicked: (Group, Item) -> Unit = { _, _ -> } - - init { - recycler.adapter = adapter - addView(recycler) - - groups.forEach { it.initModels() } - } - - /** - * Returns true if there's at least one filter from [FilterGroup] active. - */ - fun hasActiveFilters(): Boolean { - return (groups[0] as FilterGroup).items.any { - when (it) { - is Item.TriStateGroup -> - if (it.resTitle == R.string.categories) it.state == STATE_IGNORE - else it.state != STATE_IGNORE - is Item.CheckboxGroup -> it.checked - else -> false - } - } - } - - /** - * Adapter of the recycler view. - */ - inner class Adapter(items: List) : ExtendedNavigationView.Adapter(items) { - - override fun onItemClicked(item: Item) { - if (item is GroupedItem) { - item.group.onItemClicked(item) - onGroupClicked(item.group, item) - } - } - } - - /** - * Filters group (unread, downloaded, ...). - */ - inner class FilterGroup : Group { - - private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this) - - private val unread = Item.TriStateGroup(R.string.action_filter_unread, this) - - private val completed = Item.TriStateGroup(R.string.completed, this) - - private val tracked = Item.TriStateGroup(R.string.tracked, this) - - private val categories = Item.TriStateGroup(R.string.action_hide_categories, this) - - override val items:List = { - val list = mutableListOf() - if (Injekt.get().getCategories().executeAsBlocking().isNotEmpty()) - list.add(categories) - list.add(downloaded) - list.add(unread) - list.add(completed) - if (Injekt.get().hasLoggedServices()) - list.add(tracked) - list - }() - - override val header = Item.Header(R.string.action_filter) - - override val footer = Item.Separator() - - override fun initModels() { - try { - categories.state = - if (preferences.hideCategories().getOrDefault()) STATE_INCLUDE - else STATE_IGNORE - downloaded.state = preferences.filterDownloaded().getOrDefault() - unread.state = preferences.filterUnread().getOrDefault() - completed.state = preferences.filterCompleted().getOrDefault() - tracked.state = preferences.filterTracked().getOrDefault() - } - catch (e: Exception) { - preferences.upgradeFilters() - } - } - - override fun onItemClicked(item: Item) { - if (item == categories) { - item as Item.TriStateGroup - val newState = when (item.state) { - STATE_IGNORE -> STATE_INCLUDE - else -> STATE_IGNORE - } - item.state = newState - when (item) { - categories -> preferences.hideCategories().set(item.state == STATE_INCLUDE) - } - } - else if (item is Item.TriStateGroup) { - val newState = when (item.state) { - STATE_IGNORE -> STATE_INCLUDE - STATE_INCLUDE -> STATE_EXCLUDE - else -> STATE_IGNORE - } - item.state = newState - when (item) { - downloaded -> preferences.filterDownloaded().set(item.state) - unread -> preferences.filterUnread().set(item.state) - completed -> preferences.filterCompleted().set(item.state) - tracked -> preferences.filterTracked().set(item.state) - } - } - adapter.notifyItemChanged(item) - } - } - - /** - * Sorting group (alphabetically, by last read, ...) and ascending or descending. - */ - inner class SortGroup : Group { - - private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) - - private val total = Item.MultiSort(R.string.action_sort_total, this) - - private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this) - - private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this) - - private val unread = Item.MultiSort(R.string.action_filter_unread, this) - - private val dragAndDrop = Item.MultiSort(R.string.action_sort_drag_and_drop, this) - - override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, - dragAndDrop) - - override val header = Item.Header(R.string.action_sort) - - override val footer = Item.Separator() - - override fun initModels() { - val sorting = preferences.librarySortingMode().getOrDefault() - val order = if (preferences.librarySortingAscending().getOrDefault()) - SORT_ASC else SORT_DESC - - alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE - lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE - lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE - unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE - total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE - dragAndDrop.state = if (sorting == LibrarySort.DRAG_AND_DROP) order else SORT_NONE - } - - override fun onItemClicked(item: Item) { - item as Item.MultiStateGroup - val prevState = item.state - - item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE } - if (item == dragAndDrop) - item.state = SORT_ASC - else - item.state = when (prevState) { - SORT_NONE -> SORT_ASC - SORT_ASC -> SORT_DESC - SORT_DESC -> SORT_ASC - else -> throw Exception("Unknown state") - } - - preferences.librarySortingMode().set(when (item) { - alphabetically -> LibrarySort.ALPHA - lastRead -> LibrarySort.LAST_READ - lastUpdated -> LibrarySort.LAST_UPDATED - unread -> LibrarySort.UNREAD - total -> LibrarySort.TOTAL - dragAndDrop -> LibrarySort.DRAG_AND_DROP - else -> LibrarySort.ALPHA - }) - preferences.librarySortingAscending().set(item.state == SORT_ASC) - - item.group.items.forEach { adapter.notifyItemChanged(it) } - } - - } - - inner class BadgeGroup : Group { - private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this) - override val header = null - override val footer = null - override val items = listOf(downloadBadge) - override fun initModels() { - downloadBadge.checked = preferences.downloadBadge().getOrDefault() - } - - override fun onItemClicked(item: Item) { - item as Item.CheckboxGroup - item.checked = !item.checked - preferences.downloadBadge().set((item.checked)) - adapter.notifyItemChanged(item) - } - } - - /** - * Display group, to show the library as a list or a grid. - */ - inner class DisplayGroup : Group { - - private val grid = Item.Radio(R.string.action_display_grid, this) - - private val list = Item.Radio(R.string.action_display_list, this) - - override val items = listOf(grid, list) - - override val header = Item.Header(R.string.action_display) - - override val footer = null - - override fun initModels() { - val asList = preferences.libraryAsList().getOrDefault() - grid.checked = !asList - list.checked = asList - } - - override fun onItemClicked(item: Item) { - item as Item.Radio - if (item.checked) return - - item.group.items.forEach { (it as Item.Radio).checked = false } - item.checked = true - - preferences.libraryAsList().set(item == list) - - item.group.items.forEach { adapter.notifyItemChanged(it) } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index f70dad6f4a..ded7f9b5ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -1,18 +1,11 @@ package eu.kanade.tachiyomi.ui.library -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Category.Companion.ALPHA_ASC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.ALPHA_DSC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.LAST_READ_ASC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.LAST_READ_DSC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.UNREAD_ASC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.UNREAD_DSC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.UPDATED_ASC -import eu.kanade.tachiyomi.data.database.models.Category.Companion.UPDATED_DSC +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.download.DownloadManager @@ -20,54 +13,46 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -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.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.migration.MigrationFlags -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource -import eu.kanade.tachiyomi.util.lang.combineLatest -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed +import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet import eu.kanade.tachiyomi.util.lang.removeArticles +import eu.kanade.tachiyomi.util.system.executeOnIO +import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_EXCLUDE import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_INCLUDE -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_REALLY_EXCLUDE +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.ArrayList import java.util.Collections import java.util.Comparator -/** - * Class containing library information. - */ -private data class Library(val categories: List, val mangaMap: LibraryMap) - -/** - * Typealias for the library manga, using the category as keys, and list of manga as values. - */ -private typealias LibraryMap = Map> - /** * Presenter of [LibraryController]. */ class LibraryPresenter( - private val db: DatabaseHelper = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get(), - private val coverCache: CoverCache = Injekt.get(), - private val sourceManager: SourceManager = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get() -) : BasePresenter() { + private val view: LibraryController, + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) { + + private var scope = CoroutineScope(Job() + Dispatchers.Default) private val context = preferences.context private val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } + /** * Categories of the library. */ @@ -76,50 +61,48 @@ class LibraryPresenter( var allCategories: List = emptyList() private set - /** - * Relay used to apply the UI filters to the last emission of the library. - */ - private val filterTriggerRelay = BehaviorRelay.create(Unit) /** - * Relay used to apply the UI update to the last emission of the library. + * List of all manga to update the */ - private val downloadTriggerRelay = BehaviorRelay.create(Unit) + var libraryItems: List = emptyList() + private var allLibraryItems: List = emptyList() - /** - * Relay used to apply the selected sorting method to the last emission of the library. - */ - private val sortTriggerRelay = BehaviorRelay.create(Unit) + private var totalChapters: Map? = null - /** - * Library subscription. - */ - private var librarySubscription: Subscription? = null + fun onDestroy() { + lastLibraryItems = libraryItems + lastCategories = categories + } - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - subscribeLibrary() + fun onRestore() { + libraryItems = lastLibraryItems ?: return + categories = lastCategories ?: return + lastCategories = null + lastLibraryItems = null } - /** - * Subscribes to library if needed. - */ - fun subscribeLibrary() { - if (librarySubscription.isNullOrUnsubscribed()) { - librarySubscription = getLibraryObservable() - .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io())) { - lib, _ -> lib.apply { setDownloadCount(mangaMap) } - } - .combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { - lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) - } - .combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { - lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, (categories, mangaMap) -> - view.onNextLibraryUpdate(categories, mangaMap) - }) + fun getLibrary() { + launchUI { + totalChapters = null + val mangaMap = withContext(Dispatchers.IO) { + val library = getLibraryFromDB() + library.apply { + setDownloadCount(library) + setUnreadBadge(library) + } + allLibraryItems = library + var mangaMap = library + mangaMap = applyFilters(mangaMap) + mangaMap = applySort(mangaMap) + mangaMap + } + val freshStart = libraryItems.isEmpty() + libraryItems = mangaMap + view.onNextLibraryUpdate(libraryItems, freshStart) + withContext(Dispatchers.IO) { + setTotalChapters() + } } } @@ -128,7 +111,7 @@ class LibraryPresenter( * * @param map the map to filter. */ - private fun applyFilters(map: LibraryMap): LibraryMap { + private fun applyFilters(map: List): List { val filterDownloaded = preferences.filterDownloaded().getOrDefault() val filterUnread = preferences.filterUnread().getOrDefault() @@ -137,27 +120,64 @@ class LibraryPresenter( val filterTracked = preferences.filterTracked().getOrDefault() - val filterFn: (LibraryItem) -> Boolean = f@ { item -> - // Filter when there isn't unread chapters. - if (filterUnread == STATE_INCLUDE && item.manga.unread == 0) return@f false - if (filterUnread == STATE_EXCLUDE && item.manga.unread > 0) return@f false + val filterMangaType = preferences.filterMangaType().getOrDefault() + + val filterTrackers = FilterBottomSheet.FILTER_TRACKER - if (filterCompleted == STATE_INCLUDE && item.manga.status != SManga.COMPLETED) - return@f false - if (filterCompleted == STATE_EXCLUDE && item.manga.status == SManga.COMPLETED) - return@f false + return map.filter f@{ item -> + if (item.manga.isBlank()) { + return@f filterDownloaded == 0 && filterUnread == 0 && filterCompleted == 0 && + filterTracked == 0 && filterMangaType == 0 + } + // Filter for unread chapters + if (filterUnread == STATE_INCLUDE && (item.manga.unread == 0 || db.getChapters(item.manga) + .executeAsBlocking().size != item.manga.unread) + ) return@f false + if (filterUnread == STATE_EXCLUDE && (item.manga.unread == 0 || db.getChapters(item.manga) + .executeAsBlocking().size == item.manga.unread) + ) return@f false + if (filterUnread == STATE_REALLY_EXCLUDE && item.manga.unread > 0) return@f false + + if (filterMangaType > 0) { + if (if (filterMangaType == Manga.TYPE_MANHWA) (filterMangaType != item.manga.mangaType() && filterMangaType != Manga.TYPE_WEBTOON) + else filterMangaType != item.manga.mangaType() + ) return@f false + } + // Filter for completed status of manga + if (filterCompleted == STATE_INCLUDE && item.manga.status != SManga.COMPLETED) return@f false + if (filterCompleted == STATE_EXCLUDE && item.manga.status == SManga.COMPLETED) return@f false + + // Filter for tracked (or per tracked service) if (filterTracked != STATE_IGNORE) { - val db = Injekt.get() val tracks = db.getTracks(item.manga).executeAsBlocking() - val trackCount = loggedServices.count { service -> + val hasTrack = loggedServices.any { service -> tracks.any { it.sync_id == service.id } } - if (filterTracked == STATE_INCLUDE && trackCount == 0) return@f false - if (filterTracked == STATE_EXCLUDE && trackCount > 0) return@f false + val service = if (filterTrackers.isNotEmpty()) loggedServices.find { + it.name == filterTrackers + } else null + if (filterTracked == STATE_INCLUDE) { + if (!hasTrack) return@f false + if (filterTrackers.isNotEmpty()) { + if (service != null) { + val hasServiceTrack = tracks.any { it.sync_id == service.id } + if (!hasServiceTrack) return@f false + if (filterTracked == STATE_EXCLUDE && hasServiceTrack) return@f false + } + } + } else if (filterTracked == STATE_EXCLUDE) { + if (!hasTrack && filterTrackers.isEmpty()) return@f false + if (filterTrackers.isNotEmpty()) { + if (service != null) { + val hasServiceTrack = tracks.any { it.sync_id == service.id } + if (hasServiceTrack) return@f false + } + } + } } - // Filter when there are no downloads. + // Filter for downloaded manga if (filterDownloaded != STATE_IGNORE) { val isDownloaded = when { item.manga.source == LocalSource.ID -> true @@ -168,8 +188,6 @@ class LibraryPresenter( } true } - - return map.mapValues { entry -> entry.value.filter(filterFn) } } /** @@ -177,21 +195,24 @@ class LibraryPresenter( * * @param map the map of manga. */ - private fun setDownloadCount(map: LibraryMap) { + private fun setDownloadCount(itemList: List) { if (!preferences.downloadBadge().getOrDefault()) { // Unset download count if the preference is not enabled. - for ((_, itemList) in map) { - for (item in itemList) { - item.downloadCount = -1 - } + for (item in itemList) { + item.downloadCount = -1 } return } - for ((_, itemList) in map) { - for (item in itemList) { - item.downloadCount = downloadManager.getDownloadCount(item.manga) - } + for (item in itemList) { + item.downloadCount = downloadManager.getDownloadCount(item.manga) + } + } + + private fun setUnreadBadge(itemList: List) { + val unreadType = preferences.unreadBadgeType().getOrDefault() + for (item in itemList) { + item.unreadType = unreadType } } @@ -200,117 +221,160 @@ class LibraryPresenter( * * @param map the map to sort. */ - private fun applySort(map: LibraryMap): LibraryMap { + private fun applySort(map: List): List { val sortingMode = preferences.librarySortingMode().getOrDefault() val lastReadManga by lazy { var counter = 0 db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ } } - val totalChapterManga by lazy { - var counter = 0 - db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ } - } - val catListing by lazy { - val default = createDefaultCategory() - default.order = -1 - listOf(default) + db.getCategories().executeAsBlocking() - } val ascending = preferences.librarySortingAscending().getOrDefault() + val useDnD = !preferences.hideCategories().getOrDefault() val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> - val compare = when (sortingMode) { - LibrarySort.ALPHA -> sortAlphabetical(i1, i2) - LibrarySort.LAST_READ -> { + if (!(sortingMode == LibrarySort.DRAG_AND_DROP || useDnD)) { + i1.chapterCount = -1 + i2.chapterCount = -1 + } + val compare = when { + sortingMode == LibrarySort.DRAG_AND_DROP || useDnD -> + sortCategory(i1, i2, lastReadManga) + sortingMode == LibrarySort.ALPHA -> sortAlphabetical(i1, i2) + sortingMode == LibrarySort.LAST_READ -> { // Get index of manga, set equal to list if size unknown. val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size manga1LastRead.compareTo(manga2LastRead) } - LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update) - LibrarySort.UNREAD -> + sortingMode == LibrarySort.LATEST_CHAPTER -> i2.manga.last_update.compareTo(i1 + .manga.last_update) + sortingMode == LibrarySort.UNREAD -> when { i1.manga.unread == i2.manga.unread -> 0 i1.manga.unread == 0 -> if (ascending) 1 else -1 i2.manga.unread == 0 -> if (ascending) -1 else 1 else -> i1.manga.unread.compareTo(i2.manga.unread) } - LibrarySort.TOTAL -> { - val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0 - val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0 + sortingMode == LibrarySort.TOTAL -> { + setTotalChapters() + val manga1TotalChapter = totalChapters!![i1.manga.id!!] ?: 0 + val mange2TotalChapter = totalChapters!![i2.manga.id!!] ?: 0 + i1.chapterCount = totalChapters!![i1.manga.id!!] ?: 0 + i2.chapterCount = totalChapters!![i2.manga.id!!] ?: 0 manga1TotalChapter.compareTo(mange2TotalChapter) } - LibrarySort.DRAG_AND_DROP -> { - if (i1.manga.category == i2.manga.category) { - val category = catListing.find { it.id == i1.manga.category } - when { - category?.mangaSort != null -> { - var sort = when (category.mangaSort) { - ALPHA_ASC, ALPHA_DSC -> sortAlphabetical(i1, i2) - UPDATED_ASC, UPDATED_DSC -> - i2.manga.last_update.compareTo(i1.manga.last_update) - UNREAD_ASC, UNREAD_DSC -> - when { - i1.manga.unread == i2.manga.unread -> 0 - i1.manga.unread == 0 -> - if (category.isAscending()) 1 else -1 - i2.manga.unread == 0 -> - if (category.isAscending()) -1 else 1 - else -> i1.manga.unread.compareTo(i2.manga.unread) - } - LAST_READ_ASC, LAST_READ_DSC -> { - val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size - val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size - manga1LastRead.compareTo(manga2LastRead) - } - else -> sortAlphabetical(i1, i2) - } - if (!category.isAscending()) - sort *= -1 - sort - } - category?.mangaOrder?.isEmpty() == false -> { - val order = category.mangaOrder - val index1 = order.indexOf(i1.manga.id!!) - val index2 = order.indexOf(i2.manga.id!!) - when { - index1 == index2 -> 0 - index1 == -1 -> -1 - index2 == -1 -> 1 - else -> index1.compareTo(index2) - } - } - else -> 0 - } - } - else { - val category = catListing.find { it.id == i1.manga.category }?.order ?: -1 - val category2 = catListing.find { it.id == i2.manga.category }?.order ?: -1 - category.compareTo(category2) - } + sortingMode == LibrarySort.DATE_ADDED -> { + i2.manga.date_added.compareTo(i1.manga.date_added) } else -> 0 } if (compare == 0) { if (ascending) sortAlphabetical(i1, i2) else sortAlphabetical(i2, i1) - } - else compare + } else compare } - val comparator = if (ascending) + val comparator = if (ascending || useDnD) Comparator(sortFn) else Collections.reverseOrder(sortFn) - return map.mapValues { entry -> entry.value.sortedWith(comparator) } + return map.sortedWith(comparator) + } + + private fun setTotalChapters() { + if (totalChapters != null) return + val mangaMap = allLibraryItems + totalChapters = mangaMap.map { + it.manga.id!! to db.getChapters(it.manga).executeAsBlocking().size + }.toMap() + } + + private fun getCategory(categoryId: Int): Category { + val category = categories.find { it.id == categoryId } ?: createDefaultCategory() + if (category.isFirst == null) { + category.isFirst = (category.id ?: 0 <= 0 || + (category.order == 0 && categories.none { it.id == 0 })) + } + if (category.isLast == null) category.isLast = categories.lastOrNull()?.id == category.id + return category + } + + private fun sortCategory( + i1: LibraryItem, + i2: LibraryItem, + lastReadManga: Map, + initCat: Category? = null + ): Int { + return if (initCat != null || i1.manga.category == i2.manga.category) { + val category = initCat ?: allCategories.find { it.id == i1.manga.category } ?: return 0 + if (category.mangaOrder.isNullOrEmpty() && category.mangaSort == null) { + category.changeSortTo(preferences.librarySortingMode().getOrDefault()) + if (category.id == 0) preferences.defaultMangaOrder() + .set(category.mangaSort.toString()) + else db.insertCategory(category).asRxObservable().subscribe() + } + i1.chapterCount = -1 + i2.chapterCount = -1 + val compare = when { + category.mangaSort != null -> { + var sort = when (category.sortingMode()) { + LibrarySort.ALPHA -> sortAlphabetical(i1, i2) + LibrarySort.LATEST_CHAPTER -> i2.manga.last_update.compareTo(i1.manga.last_update) + LibrarySort.UNREAD -> when { + i1.manga.unread == i2.manga.unread -> 0 + i1.manga.unread == 0 -> if (category.isAscending()) 1 else -1 + i2.manga.unread == 0 -> if (category.isAscending()) -1 else 1 + else -> i1.manga.unread.compareTo(i2.manga.unread) + } + LibrarySort.LAST_READ -> { + val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size + val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size + manga1LastRead.compareTo(manga2LastRead) + } + LibrarySort.TOTAL -> { + setTotalChapters() + val manga1TotalChapter = totalChapters!![i1.manga.id!!] ?: 0 + val mange2TotalChapter = totalChapters!![i2.manga.id!!] ?: 0 + i1.chapterCount = totalChapters!![i1.manga.id!!] ?: 0 + i2.chapterCount = totalChapters!![i2.manga.id!!] ?: 0 + manga1TotalChapter.compareTo(mange2TotalChapter) + } + LibrarySort.DATE_ADDED -> i2.manga.date_added.compareTo(i1.manga.date_added) + else -> sortAlphabetical(i1, i2) + } + if (!category.isAscending()) sort *= -1 + sort + } + category.mangaOrder.isNotEmpty() -> { + val order = category.mangaOrder + val index1 = order.indexOf(i1.manga.id!!) + val index2 = order.indexOf(i2.manga.id!!) + when { + index1 == index2 -> 0 + index1 == -1 -> -1 + index2 == -1 -> 1 + else -> index1.compareTo(index2) + } + } + else -> 0 + } + if (compare == 0) { + if (category.isAscending()) sortAlphabetical(i1, i2) + else sortAlphabetical(i2, i1) + } else compare + } else { + val category = allCategories.find { it.id == i1.manga.category }?.order ?: -1 + val category2 = allCategories.find { it.id == i2.manga.category }?.order ?: -1 + category.compareTo(category2) + } } private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int { return if (preferences.removeArticles().getOrDefault()) - i1.manga.currentTitle().removeArticles().compareTo(i2.manga.currentTitle().removeArticles(), true) - else i1.manga.currentTitle().compareTo(i2.manga.currentTitle(), true) + i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true) + else i1.manga.title.compareTo(i2.manga.title, true) } /** @@ -318,89 +382,122 @@ class LibraryPresenter( * * @return an observable of the categories and its manga. */ - private fun getLibraryObservable(): Observable { - return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga -> - val categories = if (libraryManga.containsKey(0)) - arrayListOf(createDefaultCategory()) + dbCategories - else dbCategories - - this.allCategories = categories - this.categories = if (preferences.hideCategories().getOrDefault()) - arrayListOf(createDefaultCategory()) - else categories - Library(this.categories, libraryManga) + private fun getLibraryFromDB(): List { + val categories = db.getCategories().executeAsBlocking().toMutableList() + val libraryLayout = preferences.libraryLayout() + val showCategories = !preferences.hideCategories().getOrDefault() + var libraryManga = db.getLibraryMangas().executeAsBlocking() + val seekPref = preferences.alwaysShowSeeker() + if (!showCategories) + libraryManga = libraryManga.distinctBy { it.id } + val categoryAll = Category.createAll(context, + preferences.librarySortingMode().getOrDefault(), + preferences.librarySortingAscending().getOrDefault()) + val catItemAll = LibraryHeaderItem({ categoryAll }, -1, seekPref) + val categorySet = mutableSetOf() + val headerItems = (categories.mapNotNull { category -> + val id = category.id + if (id == null) null + else id to LibraryHeaderItem({ getCategory(id) }, id, seekPref) + } + (-1 to catItemAll) + + (0 to LibraryHeaderItem({ getCategory(0) }, 0, seekPref))).toMap() + val items = libraryManga.map { + val headerItem = if (!showCategories) catItemAll else + headerItems[it.category] + categorySet.add(it.category) + LibraryItem(it, libraryLayout, preferences.uniformGrid(), seekPref, headerItem) + }.toMutableList() + + if (showCategories) { + categories.forEach { category -> + if (category.id ?: 0 <= 0 && !categorySet.contains(category.id)) { + val headerItem = headerItems[category.id ?: 0] + items.add(LibraryItem( + LibraryManga.createBlank(category.id!!), + libraryLayout, + preferences.uniformGrid(), + preferences.alwaysShowSeeker(), + headerItem + )) + } + } } + + if (categories.size == 1 && showCategories) categories.first().name = + context.getString(R.string.library) + + if (categorySet.contains(0)) + categories.add(0, createDefaultCategory()) + + this.allCategories = categories + this.categories = if (!showCategories) arrayListOf(categoryAll) + else categories + + return items } private fun createDefaultCategory(): Category { val default = Category.createDefault(context) + default.order = -1 val defOrder = preferences.defaultMangaOrder().getOrDefault() if (defOrder.firstOrNull()?.isLetter() == true) default.mangaSort = defOrder.first() else default.mangaOrder = defOrder.split("/").mapNotNull { it.toLongOrNull() } return default } - /** - * Get the categories from the database. - * - * @return an observable of the categories. - */ - private fun getCategoriesObservable(): Observable> { - return db.getCategories().asRxObservable() - } - - /** - * Get the manga grouped by categories. - * - * @return an observable containing a map with the category id as key and a list of manga as the - * value. - */ - private fun getLibraryMangasObservable(): Observable { - val libraryAsList = preferences.libraryAsList() - return db.getLibraryMangas().asRxObservable() - .map { list -> - if (!preferences.hideCategories().getOrDefault()) { - list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category } - } - else { - list.distinctBy { it.id }.map { LibraryItem(it, libraryAsList)}.groupBy { - 0 } - } - } - } - /** * Requests the library to be filtered. */ fun requestFilterUpdate() { - filterTriggerRelay.call(Unit) + launchUI { + var mangaMap = allLibraryItems + mangaMap = withContext(Dispatchers.IO) { applyFilters(mangaMap) } + mangaMap = withContext(Dispatchers.IO) { applySort(mangaMap) } + libraryItems = mangaMap + view.onNextLibraryUpdate(libraryItems) + } } /** - * Requests the library to have download badges added. + * Requests the library to have download badges added/removed. */ fun requestDownloadBadgesUpdate() { - downloadTriggerRelay.call(Unit) + launchUI { + val mangaMap = allLibraryItems + withContext(Dispatchers.IO) { setDownloadCount(mangaMap) } + allLibraryItems = mangaMap + val current = libraryItems + withContext(Dispatchers.IO) { setDownloadCount(current) } + libraryItems = current + view.onNextLibraryUpdate(libraryItems) + } } /** - * Requests the library to be sorted. + * Requests the library to have unread badges changed. */ - fun requestSortUpdate() { - sortTriggerRelay.call(Unit) - } - - fun requestFullUpdate() { - librarySubscription?.unsubscribe() - subscribeLibrary() + fun requestUnreadBadgesUpdate() { + launchUI { + val mangaMap = allLibraryItems + withContext(Dispatchers.IO) { setUnreadBadge(mangaMap) } + allLibraryItems = mangaMap + val current = libraryItems + withContext(Dispatchers.IO) { setUnreadBadge(current) } + libraryItems = current + view.onNextLibraryUpdate(libraryItems) + } } /** - * Called when a manga is opened. + * Requests the library to be sorted. */ - fun onOpenManga() { - // Avoid further db updates for the library when it's not needed - librarySubscription?.let { remove(it) } + private fun requestSortUpdate() { + launchUI { + var mangaMap = libraryItems + mangaMap = withContext(Dispatchers.IO) { applySort(mangaMap) } + libraryItems = mangaMap + view.onNextLibraryUpdate(libraryItems) + } } /** @@ -412,48 +509,64 @@ class LibraryPresenter( if (mangas.isEmpty()) return emptyList() return mangas.toSet() .map { db.getCategoriesForManga(it).executeAsBlocking() } - .reduce { set1: Iterable, set2 -> set1.intersect(set2) } + .reduce { set1: Iterable, set2 -> set1.intersect(set2).toMutableList() } } /** * Remove the selected manga from the library. * * @param mangas the list of manga to delete. - * @param deleteChapters whether to also delete downloaded chapters. */ fun removeMangaFromLibrary(mangas: List) { - // Create a set of the list - val mangaToDelete = mangas.distinctBy { it.id } - mangaToDelete.forEach { it.favorite = false } - - Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } - .onErrorResumeNext { Observable.empty() } - .subscribeOn(Schedulers.io()) - .subscribe() + scope.launch { + // Create a set of the list + val mangaToDelete = mangas.distinctBy { it.id } + mangaToDelete.forEach { it.favorite = false } + + db.insertMangas(mangaToDelete).executeOnIO() + getLibrary() + } } fun confirmDeletion(mangas: List) { - Observable.fromCallable { + scope.launch { val mangaToDelete = mangas.distinctBy { it.id } mangaToDelete.forEach { manga -> - db.resetMangaInfo(manga).executeAsBlocking() + db.resetMangaInfo(manga).executeOnIO() coverCache.deleteFromCache(manga.thumbnail_url) val source = sourceManager.get(manga.source) as? HttpSource if (source != null) downloadManager.deleteManga(manga, source) } - }.subscribeOn(Schedulers.io()).subscribe() + } } - fun addMangas(mangas: List) { - val mangaToAdd = mangas.distinctBy { it.id } - mangaToAdd.forEach { it.favorite = true } + fun updateManga(manga: LibraryManga) { + scope.launch { + val rawMap = allLibraryItems + val currentMap = libraryItems + val id = manga.id ?: return@launch + val dbManga = db.getLibraryManga(id).executeOnIO() ?: return@launch + arrayOf(rawMap, currentMap).forEach { map -> + map.forEach { item -> + if (item.manga.id == dbManga.id) { + item.manga.last_update = dbManga.last_update + item.manga.unread = dbManga.unread + } + } + } + getLibrary() + } + } - Observable.fromCallable { db.insertMangas(mangaToAdd).executeAsBlocking() } - .onErrorResumeNext { Observable.empty() } - .subscribeOn(Schedulers.io()) - .subscribe() - mangaToAdd.forEach { db.insertManga(it).executeAsBlocking() } + fun reAddMangas(mangas: List) { + scope.launch { + val mangaToAdd = mangas.distinctBy { it.id } + mangaToAdd.forEach { it.favorite = true } + db.insertMangas(mangaToAdd).executeOnIO() + getLibrary() + mangaToAdd.forEach { db.insertManga(it).executeAsBlocking() } + } } /** @@ -470,89 +583,100 @@ class LibraryPresenter( mc.add(MangaCategory.create(manga, cat)) } } - db.setMangaCategories(mc, mangas) + getLibrary() } - fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) { - val source = sourceManager.get(manga.source) ?: return + fun getFirstUnread(manga: Manga): Chapter? { + val chapters = db.getChapters(manga).executeAsBlocking() + return chapters.sortedByDescending { it.source_order }.find { !it.read } + } - //state = state.copy(isReplacingManga = true) + fun sortCategory(catId: Int, order: Int) { + val category = categories.find { catId == it.id } ?: return + category.mangaSort = ('a' + (order - 1)) + if (catId == -1) { + val sort = category.sortingMode() ?: LibrarySort.ALPHA + preferences.librarySortingMode().set(sort) + preferences.librarySortingAscending().set(category.isAscending()) + requestSortUpdate() + } else { + if (category.id == 0) preferences.defaultMangaOrder().set(category.mangaSort.toString()) + else Injekt.get().insertCategory(category).executeAsBlocking() + requestSortUpdate() + } + } - Observable.defer { source.fetchChapterList(manga) } - .onErrorReturn { emptyList() } - .doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) } - .onErrorReturn { emptyList() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - //.doOnUnsubscribe { state = state.copy(isReplacingManga = false) } - .subscribe() + fun rearrangeCategory(catId: Int?, mangaIds: List) { + scope.launch { + val category = categories.find { catId == it.id } ?: return@launch + category.mangaSort = null + category.mangaOrder = mangaIds + if (category.id == 0) preferences.defaultMangaOrder().set(mangaIds.joinToString("/")) + else db.insertCategory(category).executeOnIO() + requestSortUpdate() + } } - fun hideShowTitle(mangas: List, hide: Boolean) { - mangas.forEach { it.hide_title = hide } - db.inTransaction { - mangas.forEach { - db.updateMangaHideTitle(it).executeAsBlocking() + fun moveMangaToCategory( + manga: LibraryManga, + catId: Int?, + mangaIds: List + ) { + scope.launch { + val categoryId = catId ?: return@launch + val category = categories.find { catId == it.id } ?: return@launch + + val oldCatId = manga.category + manga.category = categoryId + + val mc = ArrayList() + val categories = + if (catId == 0) emptyList() + else + db.getCategoriesForManga(manga).executeOnIO() + .filter { it.id != oldCatId } + listOf(category) + + for (cat in categories) { + mc.add(MangaCategory.create(manga, cat)) + } + + db.setMangaCategories(mc, listOf(manga)) + + if (category.mangaSort == null) { + val ids = mangaIds.toMutableList() + if (!ids.contains(manga.id!!)) ids.add(manga.id!!) + category.mangaOrder = ids + if (category.id == 0) preferences.defaultMangaOrder() + .set(mangaIds.joinToString("/")) + else db.insertCategory(category).executeAsBlocking() } + getLibrary() } } - private fun migrateMangaInternal(source: Source, sourceChapters: List, - prevManga: Manga, manga: Manga, replace: Boolean) { - - val flags = preferences.migrateFlags().getOrDefault() - val migrateChapters = MigrationFlags.hasChapters(flags) - val migrateCategories = MigrationFlags.hasCategories(flags) - val migrateTracks = MigrationFlags.hasTracks(flags) - - db.inTransaction { - // Update chapters read - if (migrateChapters) { - try { - syncChaptersWithSource(db, sourceChapters, manga, source) - } catch (e: Exception) { - // Worst case, chapters won't be synced - } + fun mangaIsInCategory(manga: LibraryManga, catId: Int?): Boolean { + val categories = db.getCategoriesForManga(manga).executeAsBlocking().map { it.id } + return catId in categories + } - val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() - val maxChapterRead = - prevMangaChapters.filter { it.read }.maxBy { it.chapter_number }?.chapter_number - if (maxChapterRead != null) { - val dbChapters = db.getChapters(manga).executeAsBlocking() - for (chapter in dbChapters) { - if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) { - chapter.read = true - } + companion object { + private var lastLibraryItems: List? = null + private var lastCategories: List? = null + + fun updateDB() { + val db: DatabaseHelper = Injekt.get() + db.inTransaction { + val libraryManga = db.getLibraryMangas().executeAsBlocking() + libraryManga.forEach { manga -> + if (manga.date_added == 0L) { + val chapters = db.getChapters(manga).executeAsBlocking() + manga.date_added = chapters.minBy { it.date_fetch }?.date_fetch ?: 0L + db.insertManga(manga).executeAsBlocking() } - db.insertChapters(dbChapters).executeAsBlocking() + db.resetMangaInfo(manga).executeAsBlocking() } } - // Update categories - if (migrateCategories) { - val categories = db.getCategoriesForManga(prevManga).executeAsBlocking() - val mangaCategories = categories.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mangaCategories, listOf(manga)) - } - // Update track - if (migrateTracks) { - val tracks = db.getTracks(prevManga).executeAsBlocking() - for (track in tracks) { - track.id = null - track.manga_id = manga.id!! - } - db.insertTracks(tracks).executeAsBlocking() - } - // Update favorite status - if (replace) { - prevManga.favorite = false - db.updateMangaFavorite(prevManga).executeAsBlocking() - } - manga.favorite = true - db.updateMangaFavorite(manga).executeAsBlocking() - - // SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title - db.updateMangaTitle(manga).executeAsBlocking() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt deleted file mode 100644 index e490e43649..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySelectionEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import eu.kanade.tachiyomi.data.database.models.Manga - -sealed class LibrarySelectionEvent { - - class Selected(val manga: Manga) : LibrarySelectionEvent() - class Unselected(val manga: Manga) : LibrarySelectionEvent() - class Cleared() : LibrarySelectionEvent() -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt index 4514afdb0c..3ad22861c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -4,8 +4,9 @@ object LibrarySort { const val ALPHA = 0 const val LAST_READ = 1 - const val LAST_UPDATED = 2 + const val LATEST_CHAPTER = 2 const val UNREAD = 3 const val TOTAL = 4 + const val DATE_ADDED = 5 const val DRAG_AND_DROP = 6 -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt new file mode 100644 index 0000000000..98d281a521 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt @@ -0,0 +1,344 @@ +package eu.kanade.tachiyomi.ui.library.filter + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.view.inflate +import eu.kanade.tachiyomi.util.view.updatePaddingRelative +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.filter_bottom_sheet.view.* +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + LinearLayout(context, attrs), + FilterTagGroupListener { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + private lateinit var downloaded: FilterTagGroup + + private lateinit var unread: FilterTagGroup + + private lateinit var completed: FilterTagGroup + + private lateinit var tracked: FilterTagGroup + + private var trackers: FilterTagGroup? = null + + private var mangaType: FilterTagGroup? = null + + var sheetBehavior: BottomSheetBehavior? = null + + private lateinit var clearButton: ImageView + + private val filterItems: MutableList by lazy { + val list = mutableListOf() + list.add(unread) + list.add(downloaded) + list.add(completed) + if (Injekt.get().hasLoggedServices()) + list.add(tracked) + list + } + + var onGroupClicked: (Int) -> Unit = { _ -> } + var pager: View? = null + + fun onCreate(pagerView: View) { + clearButton = clear_button + filter_layout.removeView(clearButton) + sheetBehavior = BottomSheetBehavior.from(this) + sheetBehavior?.isHideable = true + pager = pagerView + val shadow2: View = (pagerView.parent.parent as ViewGroup).findViewById(R.id.shadow2) + val shadow: View = (pagerView.parent.parent as ViewGroup).findViewById(R.id.shadow) + sheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { + pill.alpha = (1 - max(0f, progress)) * 0.25f + shadow2.alpha = (1 - max(0f, progress)) * 0.25f + shadow.alpha = 1 + min(0f, progress) + updateRootPadding(progress) + } + + override fun onStateChanged(p0: View, state: Int) { + stateChanged(state) + } + }) + if (preferences.hideFiltersAtStart().getOrDefault()) { + sheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN + } + hide_filters.isChecked = preferences.hideFiltersAtStart().getOrDefault() + hide_filters.setOnCheckedChangeListener { _, isChecked -> + preferences.hideFiltersAtStart().set(isChecked) + if (isChecked) + onGroupClicked(ACTION_HIDE_FILTER_TIP) + } + view_options.setOnClickListener { + onGroupClicked(ACTION_DISPLAY) + } + + val activeFilters = hasActiveFiltersFromPref() + sheetBehavior?.isHideable = !activeFilters + if (activeFilters && sheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN && + sheetBehavior?.skipCollapsed == false) + sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + + post { + updateRootPadding( + when (sheetBehavior?.state) { + BottomSheetBehavior.STATE_HIDDEN -> -1f + BottomSheetBehavior.STATE_EXPANDED -> 1f + else -> 0f + } + ) + shadow.alpha = if (sheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN) 0f else 1f + } + + createTags() + clearButton.setOnClickListener { clearFilters() } + } + + private fun stateChanged(state: Int) { + val shadow = ((pager?.parent as? ViewGroup)?.findViewById(R.id.shadow) as? View) + if (state == BottomSheetBehavior.STATE_COLLAPSED) { + shadow?.alpha = 1f + pager?.updatePaddingRelative(bottom = sheetBehavior?.peekHeight ?: 0) + } + if (state == BottomSheetBehavior.STATE_EXPANDED) { + pill.alpha = 0f + } + if (state == BottomSheetBehavior.STATE_HIDDEN) { + reSortViews() + shadow?.alpha = 0f + pager?.updatePaddingRelative(bottom = 0) + } + } + + override fun onRestoreInstanceState(state: Parcelable?) { + super.onRestoreInstanceState(state) + val sheetBehavior = BottomSheetBehavior.from(this) + stateChanged(sheetBehavior.state) + } + + fun updateRootPadding(progress: Float? = null) { + val minHeight = sheetBehavior?.peekHeight ?: 0 + val maxHeight = height + val trueProgress = progress + ?: if (sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) 1f else 0f + val percent = (trueProgress * 100).roundToInt() + val value = (percent * (maxHeight - minHeight) / 100) + minHeight + val height = context.resources.getDimensionPixelSize(R.dimen.rounder_radius) + if (trueProgress >= 0) + pager?.updatePaddingRelative(bottom = value - height) + else + pager?.updatePaddingRelative(bottom = (minHeight * (1 + trueProgress)).toInt()) + } + + fun hasActiveFilters() = filterItems.any { it.isActivated } + + private fun hasActiveFiltersFromPref(): Boolean { + return preferences.filterDownloaded().getOrDefault() > 0 || preferences.filterUnread() + .getOrDefault() > 0 || preferences.filterCompleted() + .getOrDefault() > 0 || preferences.filterTracked() + .getOrDefault() > 0 || preferences.filterMangaType() + .getOrDefault() > 0 || FILTER_TRACKER.isNotEmpty() + } + + private fun createTags() { + hide_categories.isChecked = preferences.hideCategories().getOrDefault() + hide_categories.setOnCheckedChangeListener { _, isChecked -> + preferences.hideCategories().set(isChecked) + onGroupClicked(ACTION_REFRESH) + } + + downloaded = inflate(R.layout.filter_buttons) as FilterTagGroup + downloaded.setup(this, R.string.downloaded, R.string.not_downloaded) + + completed = inflate(R.layout.filter_buttons) as FilterTagGroup + completed.setup(this, R.string.completed, R.string.ongoing) + + unread = inflate(R.layout.filter_buttons) as FilterTagGroup + unread.setup(this, R.string.not_started, R.string.in_progress, + R.string.read) + + tracked = inflate(R.layout.filter_buttons) as FilterTagGroup + tracked.setup(this, R.string.tracked, R.string.not_tracked) + + reSortViews() + + checkForManhwa() + } + + private fun checkForManhwa() { + GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) { + val db: DatabaseHelper by injectLazy() + val showCategoriesCheckBox = withContext(Dispatchers.IO) { + db.getCategories().executeAsBlocking() + .isNotEmpty() + } + val libraryManga = db.getLibraryMangas().executeAsBlocking() + val types = mutableListOf() + if (libraryManga.any { it.mangaType() == Manga.TYPE_MANHWA }) types.add(R.string.manhwa) + if (libraryManga.any { it.mangaType() == Manga.TYPE_MANHUA }) types.add(R.string.manhua) + if (libraryManga.any { it.mangaType() == Manga.TYPE_COMIC }) types.add(R.string.comic) + val hasTracking = Injekt.get().hasLoggedServices() + if (types.isNotEmpty()) { + launchUI { + val mangaType = inflate(R.layout.filter_buttons) as FilterTagGroup + mangaType.setup( + this@FilterBottomSheet, + types.first(), + types.getOrNull(1), + types.getOrNull(2) + ) + this@FilterBottomSheet.mangaType = mangaType + filter_layout.addView(mangaType) + filterItems.remove(tracked) + filterItems.add(mangaType) + if (hasTracking) + filterItems.add(tracked) + } + } + withContext(Dispatchers.Main) { + hide_categories.visibleIf(showCategoriesCheckBox) + downloaded.setState(preferences.filterDownloaded()) + completed.setState(preferences.filterCompleted()) + unread.setState(preferences.filterUnread()) + tracked.setState(preferences.filterTracked()) + mangaType?.setState(preferences.filterMangaType()) + reSortViews() + } + + if (filterItems.contains(tracked)) { + val loggedServices = Injekt.get().services.filter { it.isLogged } + if (loggedServices.size > 1) { + val serviceNames = loggedServices.map { it.name } + withContext(Dispatchers.Main) { + trackers = inflate(R.layout.filter_buttons) as FilterTagGroup + trackers?.setup( + this@FilterBottomSheet, + serviceNames.first(), + serviceNames.getOrNull(1), + serviceNames.getOrNull(2) + ) + if (tracked.isActivated) { + filter_layout.addView(trackers) + filterItems.add(trackers!!) + trackers?.setState(FILTER_TRACKER) + reSortViews() + } + } + } + } + } + } + + override fun onFilterClicked(view: FilterTagGroup, index: Int, updatePreference: Boolean) { + if (updatePreference) { + if (view == trackers) { + FILTER_TRACKER = view.nameOf(index) ?: "" + } else { + when (view) { + downloaded -> preferences.filterDownloaded() + unread -> preferences.filterUnread() + completed -> preferences.filterCompleted() + tracked -> preferences.filterTracked() + mangaType -> preferences.filterMangaType() + else -> null + }?.set(index + 1) + } + onGroupClicked(ACTION_FILTER) + } + if (tracked.isActivated && trackers != null && trackers?.parent == null) { + filter_layout.addView(trackers) + filterItems.add(trackers!!) + } else if (!tracked.isActivated && trackers?.parent != null) { + filter_layout.removeView(trackers) + trackers?.reset() + FILTER_TRACKER = "" + filterItems.remove(trackers!!) + } + val hasFilters = hasActiveFilters() + sheetBehavior?.isHideable = !hasFilters + if (hasFilters && clearButton.parent == null) + filter_layout.addView(clearButton, 0) + else if (!hasFilters && clearButton.parent != null) + filter_layout.removeView(clearButton) + } + + fun hideIfPossible() { + if (!hasActiveFilters() && sheetBehavior?.isHideable == true) + sheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN + } + + fun canHide(): Boolean = sheetBehavior?.isHideable == true && sheetBehavior?.state != + BottomSheetBehavior.STATE_HIDDEN + + private fun clearFilters() { + preferences.filterDownloaded().set(0) + preferences.filterUnread().set(0) + preferences.filterCompleted().set(0) + preferences.filterTracked().set(0) + preferences.filterMangaType().set(0) + FILTER_TRACKER = "" + + val transition = androidx.transition.AutoTransition() + transition.duration = 150 + androidx.transition.TransitionManager.beginDelayedTransition(filter_layout, transition) + filterItems.forEach { + it.reset() + } + if (trackers != null) + filterItems.remove(trackers!!) + reSortViews() + onGroupClicked(ACTION_FILTER) + sheetBehavior?.isHideable = true + } + + private fun reSortViews() { + filter_layout.removeAllViews() + if (filterItems.any { it.isActivated }) + filter_layout.addView(clearButton) + filterItems.filter { it.isActivated }.forEach { + filter_layout.addView(it) + } + filterItems.filterNot { it.isActivated }.forEach { + filter_layout.addView(it) + } + filter_scroll.scrollTo(0, 0) + } + + companion object { + const val ACTION_REFRESH = 0 + const val ACTION_FILTER = 1 + const val ACTION_HIDE_FILTER_TIP = 2 + const val ACTION_DISPLAY = 3 + var FILTER_TRACKER = "" + private set + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterTagGroup.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterTagGroup.kt new file mode 100644 index 0000000000..3386b9c190 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterTagGroup.kt @@ -0,0 +1,145 @@ +package eu.kanade.tachiyomi.ui.library.filter + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import com.f2prateek.rx.preferences.Preference +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.android.synthetic.main.filter_buttons.view.* + +class FilterTagGroup@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout + (context, attrs) { + + private var listener: FilterTagGroupListener? = null + + var itemCount = 0 + private set + + private var root: ViewGroup? = null + + private val buttons by lazy { arrayOf(firstButton, secondButton, thirdButton) } + private val separators by lazy { arrayOf(separator1, separator2) } + + override fun isActivated(): Boolean { + return buttons.any { it.isActivated } + } + + fun nameOf(index: Int): String? = buttons.getOrNull(index)?.text as? String + + fun setup(root: ViewGroup, firstText: Int, secondText: Int? = null, thirdText: Int? = null) { + val text1 = context.getString(firstText) + val text2 = if (secondText != null) context.getString(secondText) else null + val text3 = if (thirdText != null) context.getString(thirdText) else null + setup(root, text1, text2, text3) + } + + fun setup( + root: ViewGroup, + firstText: String, + secondText: String? = null, + thirdText: String? = + null + ) { + listener = root as? FilterTagGroupListener + (layoutParams as? MarginLayoutParams)?.rightMargin = 5.dpToPx + (layoutParams as? MarginLayoutParams)?.leftMargin = 5.dpToPx + firstButton.text = firstText + if (secondText != null) { + secondButton.text = secondText + itemCount = 2 + if (thirdText != null) { + thirdButton.text = thirdText + itemCount = 3 + } else { + thirdButton.gone() + separator2.gone() + } + } else { + itemCount = 1 + secondButton.gone() + separator1.gone() + thirdButton.gone() + separator2.gone() + } + this.root = root + firstButton.setOnClickListener { toggleButton(0) } + secondButton.setOnClickListener { toggleButton(1) } + thirdButton.setOnClickListener { toggleButton(2) } + } + + fun setState(preference: Preference) { + val index = preference.getOrDefault() - 1 + if (index > -1) + toggleButton(index, false) + } + + fun setState(text: String) { + val index = buttons.indexOfFirst { it.text == text && it.visibility == View.VISIBLE } + if (index > -1) + toggleButton(index, false) + } + + fun reset() { + buttons.forEach { + it.isActivated = false + } + for (i in 0 until itemCount) { + buttons[i].visible() + buttons[i].setTextColor(context.getResourceColor(android.R.attr.textColorPrimary)) + } + for (i in 0 until (itemCount - 1)) separators[i].visible() + } + + private fun toggleButton(index: Int, callBack: Boolean = true) { + if (itemCount == 0) return + if (callBack) { + val transition = androidx.transition.AutoTransition() + transition.duration = 150 + androidx.transition.TransitionManager.beginDelayedTransition( + parent.parent as ViewGroup, transition + ) + } + if (itemCount == 1) { + firstButton.isActivated = !firstButton.isActivated + firstButton.setTextColor(if (firstButton.isActivated) Color.WHITE else context + .getResourceColor(android.R.attr.textColorPrimary)) + listener?.onFilterClicked(this, if (firstButton.isActivated) index else -1, callBack) + return + } + val buttons = mutableListOf(firstButton, secondButton) + if (itemCount >= 3) + buttons.add(thirdButton) + val mainButton = buttons[index] + buttons.remove(mainButton) + + if (mainButton.isActivated) { + mainButton.isActivated = false + separator1.visible() + listener?.onFilterClicked(this, -1, callBack) + if (itemCount >= 3) + separator2.visible() + buttons.forEach { it.visible() } + } else { + mainButton.isActivated = true + listener?.onFilterClicked(this, index, callBack) + buttons.forEach { it.gone() } + separator1.gone() + if (itemCount >= 3) { + separator2.gone() + } + } + mainButton.setTextColor(if (mainButton.isActivated) Color.WHITE else context + .getResourceColor(android.R.attr.textColorPrimary)) + } +} + +interface FilterTagGroupListener { + fun onFilterClicked(view: FilterTagGroup, index: Int, updatePreference: Boolean) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d34c7a0c11..0dfede35f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -1,91 +1,99 @@ package eu.kanade.tachiyomi.ui.main -import android.animation.ObjectAnimator +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator import android.app.SearchManager import android.content.Intent -import android.content.res.Configuration import android.graphics.Color import android.graphics.Rect +import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.provider.Settings +import android.view.GestureDetector +import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowManager import android.webkit.WebView -import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.TextView import androidx.appcompat.graphics.drawable.DrawerArrowDrawable -import androidx.biometric.BiometricManager +import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils -import androidx.core.view.GravityCompat +import androidx.core.view.GestureDetectorCompat +import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.Conductor import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.google.android.material.snackbar.Snackbar +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.DownloadServiceListener import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.ui.base.activity.BaseActivity +import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController -import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController -import eu.kanade.tachiyomi.ui.download.DownloadController -import eu.kanade.tachiyomi.ui.extension.ExtensionController import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController +import eu.kanade.tachiyomi.ui.recents.RecentsController +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate +import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.ui.setting.SettingsMainController +import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.marginTop import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePadding -import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import eu.kanade.tachiyomi.util.view.visible -import eu.kanade.tachiyomi.util.system.openInBrowser import kotlinx.android.synthetic.main.main_activity.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.util.Date import java.util.concurrent.TimeUnit +import kotlin.math.abs -open class MainActivity : BaseActivity() { +open class MainActivity : BaseActivity(), DownloadServiceListener { protected lateinit var router: Router - protected var drawerArrow: DrawerArrowDrawable? = null - - protected open var trulyGoBack = false + var drawerArrow: DrawerArrowDrawable? = null + private set + private var searchDrawable: Drawable? = null + private var dismissDrawable: Drawable? = null + private lateinit var gestureDetector: GestureDetectorCompat - private var secondaryDrawer: ViewGroup? = null - - private var snackBar:Snackbar? = null - private var extraViewForUndo:View? = null + private var snackBar: Snackbar? = null + private var extraViewForUndo: View? = null private var canDismissSnackBar = false + private var animationSet: AnimatorSet? = null + fun setUndoSnackBar(snackBar: Snackbar?, extraViewToCheck: View? = null) { this.snackBar = snackBar canDismissSnackBar = false launchUI { - delay(2000) + delay(1000) if (this@MainActivity.snackBar == snackBar) { canDismissSnackBar = true } @@ -93,125 +101,82 @@ open class MainActivity : BaseActivity() { extraViewForUndo = extraViewToCheck } - private val startScreenId by lazy { - when (preferences.startScreen()) { - 2 -> R.id.nav_drawer_recently_read - 3 -> R.id.nav_drawer_recent_updates - else -> R.id.nav_drawer_library - } - } - lateinit var tabAnimator: TabsAnimator override fun onCreate(savedInstanceState: Bundle?) { - if (preferences.theme() in 1..4) { - Timber.d("Manually instantiating WebView to avoid night mode issue.") - try { - WebView(applicationContext) - } catch (e: Exception) { - Timber.e(e, "Exception when creating webview at start") - } + // Create a webview before extensions do or else they will break night mode theme + // https://stackoverflow.com/questions/54191883 + Timber.d("Manually instantiating WebView to avoid night mode issue.") + try { + WebView(applicationContext) + } catch (e: Exception) { + Timber.e(e, "Exception when creating webview at start") } super.onCreate(savedInstanceState) - if (trulyGoBack) return // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 if (!isTaskRoot) { finish() return } + gestureDetector = GestureDetectorCompat(this, GestureListener()) setContentView(R.layout.main_activity) setSupportActionBar(toolbar) drawerArrow = DrawerArrowDrawable(this) - drawerArrow?.color = Color.WHITE - toolbar.navigationIcon = drawerArrow - - tabAnimator = TabsAnimator(tabs) + drawerArrow?.color = getResourceColor(R.attr.actionBarTintColor) + searchDrawable = ContextCompat.getDrawable( + this, R.drawable.ic_search_white_24dp + ) + dismissDrawable = ContextCompat.getDrawable( + this, R.drawable.ic_close_white_24dp + ) - // Set behavior of Navigation drawer - nav_view.setNavigationItemSelectedListener { item -> + var continueSwitchingTabs = false + bottom_nav.setOnNavigationItemSelectedListener { item -> val id = item.itemId - + val currentController = router.backstack.lastOrNull()?.controller() + if (!continueSwitchingTabs && currentController is BottomNavBarInterface) { + if (!currentController.canChangeTabs { + continueSwitchingTabs = true + this@MainActivity.bottom_nav.selectedItemId = id + }) return@setOnNavigationItemSelectedListener false + } + continueSwitchingTabs = false + if (item.itemId != R.id.nav_browse) + preferences.lastTab().set(item.itemId) val currentRoot = router.backstack.firstOrNull() if (currentRoot?.tag()?.toIntOrNull() != id) { - when (id) { - R.id.nav_drawer_library -> setRoot(LibraryController(), id) - R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) - R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) - R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) - R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id) - R.id.nav_drawer_downloads -> { - router.pushController(DownloadController().withFadeTransaction()) - } - R.id.nav_drawer_settings -> { - router.pushController(SettingsMainController().withFadeTransaction()) - } - R.id.nav_drawer_help -> { - openInBrowser(URL_HELP) - } + setRoot(when (id) { + R.id.nav_library -> LibraryController() + R.id.nav_recents -> RecentsController() + else -> CatalogueController() + }, id) + } else if (currentRoot.tag()?.toIntOrNull() == id) { + if (router.backstackSize == 1) { + val controller = + router.getControllerWithTag(id.toString()) as? BottomSheetController + controller?.toggleSheet() } } - drawer.closeDrawer(GravityCompat.START) true } - val container: ViewGroup = findViewById(R.id.controller_container) - val content: LinearLayout = findViewById(R.id.main_content) - container.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - nav_view.doOnApplyWindowInsets { v, _, _ -> - v.updatePaddingRelative( - bottom = v.marginBottom, - top = v.marginTop - ) - } - content.setOnApplyWindowInsetsListener { v, insets -> - window.navigationBarColor = - // if the os does not support light nav bar and is portrait, draw a dark translucent - // nav bar - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - (v.rootWindowInsets.systemWindowInsetLeft > 0 || - v.rootWindowInsets.systemWindowInsetRight > 0)) - // For lollipop, draw opaque nav bar - Color.BLACK - else Color.argb(179, 0, 0, 0) - } - // if the android q+ device has gesture nav, transparent nav bar - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && (v.rootWindowInsets.systemWindowInsetBottom != v.rootWindowInsets - .tappableElementInsets.bottom)) { - getColor(android.R.color.transparent) - } - // if in landscape with 2/3 button mode, fully opaque nav bar - else if (v.rootWindowInsets.systemWindowInsetLeft > 0 - || v.rootWindowInsets.systemWindowInsetRight > 0) { - getResourceColor(android.R.attr.colorBackground) - } - // if in portrait with 2/3 button mode, translucent nav bar - else { - ColorUtils.setAlphaComponent( - getResourceColor(android.R.attr.colorBackground), 179) - } - v.setPadding(insets.systemWindowInsetLeft, insets.systemWindowInsetTop, - insets.systemWindowInsetRight, 0) - insets - } - val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - if (Build.VERSION.SDK_INT >= 26 && currentNightMode == Configuration.UI_MODE_NIGHT_NO) { - content.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - } + val content: ViewGroup = findViewById(R.id.main_content) + DownloadService.addListener(this) + content.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + container.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + + supportActionBar?.setDisplayShowCustomEnabled(true) - val drawerContainer: FrameLayout = findViewById(R.id.drawer_container) - drawerContainer.setOnApplyWindowInsetsListener { v, insets -> + setNavBarColor(content.rootWindowInsets) + content.doOnApplyWindowInsets { v, insets, _ -> + setNavBarColor(insets) val contextView = window?.decorView?.findViewById(R.id.action_mode_bar) contextView?.updateLayoutParams { leftMargin = insets.systemWindowInsetLeft @@ -220,51 +185,70 @@ open class MainActivity : BaseActivity() { // Consume any horizontal insets and pad all content in. There's not much we can do // with horizontal insets v.updatePadding( - left = insets.systemWindowInsetLeft, - right = insets.systemWindowInsetRight + left = insets.systemWindowInsetLeft, right = insets.systemWindowInsetRight ) - insets.replaceSystemWindowInsets( - 0, insets.systemWindowInsetTop, - 0, insets.systemWindowInsetBottom + appbar.updatePadding( + top = insets.systemWindowInsetTop ) + bottom_nav.updatePadding(bottom = insets.systemWindowInsetBottom) } router = Conductor.attachRouter(this, container, savedInstanceState) if (!router.hasRootController()) { // Set start screen if (!handleIntentAction(intent)) { - setSelectedDrawerItem(startScreenId) + val lastItemId = bottom_nav.menu.findItem(preferences.lastTab().getOrDefault())?.itemId + bottom_nav.selectedItemId = lastItemId ?: R.id.nav_library } } toolbar.setNavigationOnClickListener { - if (router.backstackSize == 1) { - drawer.openDrawer(GravityCompat.START) - } else { - onBackPressed() - } + val rootSearchController = router.backstack.lastOrNull()?.controller() + if (rootSearchController is RootSearchInterface) { + rootSearchController.expandSearch() + } else onBackPressed() } + bottom_nav.visibility = if (router.backstackSize > 1) View.GONE else View.VISIBLE + bottom_nav.alpha = if (router.backstackSize > 1) 0f else 1f router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { - override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean, - container: ViewGroup, handler: ControllerChangeHandler) { - - syncActivityViewWithController(to, from) + override fun onChangeStarted( + to: Controller?, + from: Controller?, + isPush: Boolean, + container: ViewGroup, + handler: ControllerChangeHandler + ) { + + syncActivityViewWithController(to, from, isPush) + appbar.y = 0f } - override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean, - container: ViewGroup, handler: ControllerChangeHandler) { - + override fun onChangeCompleted( + to: Controller?, + from: Controller?, + isPush: Boolean, + container: ViewGroup, + handler: ControllerChangeHandler + ) { + appbar.y = 0f } - }) syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) + toolbar.navigationIcon = if (router.backstackSize > 1) drawerArrow else searchDrawable + (router.backstack.lastOrNull()?.controller() as? BaseController)?.setTitle() + (router.backstack.lastOrNull()?.controller() as? SettingsController)?.setTitle() + if (savedInstanceState == null) { // Show changelog if needed if (Migrations.upgrade(preferences)) { - ChangelogDialogController().showDialog(router) + if (BuildConfig.DEBUG) { + MaterialDialog(this).title(text = "Welcome to the J2K MD2 Beta").message( + text = "This beta is for testing the upcoming release. Requests for new additions for this beta will ignored (however suggestions on how to better implement a feature in this beta are welcome).\n\nFor any bugs you come across, there is a bug report button in settings.\n\nAs a reminder this is a *BETA* build; bugs may happen, features may be missing/not implemented yet, and screens can change.\n\nEnjoy and thanks for testing!" + ).positiveButton(android.R.string.ok).cancelOnTouchOutside(false).show() + } else ChangelogDialogController().showDialog(router) } } preferences.extensionUpdatesCount().asObservable().subscribe { @@ -273,51 +257,92 @@ open class MainActivity : BaseActivity() { setExtensionsBadge() } - private fun setExtensionsBadge() { + fun setDismissIcon(enabled: Boolean) { + toolbar.navigationIcon = if (enabled) dismissDrawable else searchDrawable + } - val extUpdateText: TextView = nav_view.menu.findItem( - R.id.nav_drawer_extensions - )?.actionView as? TextView ?: return + private fun setNavBarColor(insets: WindowInsets?) { + if (insets == null) return + window.navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { + // basically if in landscape on a phone + // For lollipop, draw opaque nav bar + if (insets.systemWindowInsetLeft > 0 || insets.systemWindowInsetRight > 0) + Color.BLACK + else Color.argb(179, 0, 0, 0) + } + // if the android q+ device has gesture nav, transparent nav bar + // this is here in case some crazy with a notch uses landscape + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (insets + .systemWindowInsetBottom != insets.tappableElementInsets.bottom)) { + getColor(android.R.color.transparent) + } + // if in landscape with 2/3 button mode, fully opaque nav bar + else if (insets.systemWindowInsetLeft > 0 || insets.systemWindowInsetRight > 0) { + getResourceColor(R.attr.colorPrimaryVariant) + } + // if in portrait with 2/3 button mode, translucent nav bar + else { + ColorUtils.setAlphaComponent( + getResourceColor(R.attr.colorPrimaryVariant), 179 + ) + } + } + override fun startSupportActionMode(callback: androidx.appcompat.view.ActionMode.Callback): androidx.appcompat.view.ActionMode? { + window?.statusBarColor = getResourceColor(R.attr.colorPrimaryVariant) + return super.startSupportActionMode(callback) + } + + override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) { + launchUI { + val scale = Settings.Global.getFloat( + contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f + ) + val duration = resources.getInteger(android.R.integer.config_mediumAnimTime) * scale + delay(duration.toLong()) + delay(100) + if (Color.alpha(window?.statusBarColor ?: Color.BLACK) >= 255) window?.statusBarColor = + getResourceColor( + android.R.attr.statusBarColor + ) + } + super.onSupportActionModeFinished(mode) + } + + private fun setExtensionsBadge() { val updates = preferences.extensionUpdatesCount().getOrDefault() if (updates > 0) { - extUpdateText.text = updates.toString() - extUpdateText.visible() - } - else { - extUpdateText.text = null - extUpdateText.gone() + val badge = bottom_nav.getOrCreateBadge(R.id.nav_browse) + badge.number = updates + badge.backgroundColor = getResourceColor(R.attr.badgeColor) + badge.badgeTextColor = Color.WHITE + } else { + bottom_nav.removeBadge(R.id.nav_browse) } } override fun onResume() { super.onResume() + // setting in case someone comes from the search activity to main getExtensionUpdates() - val useBiometrics = preferences.useBiometrics().getOrDefault() - if (useBiometrics && BiometricManager.from(this) - .canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { - if (!unlocked && (preferences.lockAfter().getOrDefault() <= 0 || Date().time >= - preferences.lastUnlock().getOrDefault() + 60 * 1000 * preferences.lockAfter().getOrDefault())) { - val intent = Intent(this, BiometricActivity::class.java) - startActivity(intent) - this.overridePendingTransition(0, 0) - } - } - else if (useBiometrics) - preferences.useBiometrics().set(false) + DownloadService.callListeners() } + override fun onPause() { + super.onPause() + snackBar?.dismiss() + } private fun getExtensionUpdates() { - if (Date().time >= preferences.lastExtCheck().getOrDefault() + - TimeUnit.HOURS.toMillis(1)) { + if (Date().time >= preferences.lastExtCheck().getOrDefault() + TimeUnit.HOURS.toMillis(6)) { GlobalScope.launch(Dispatchers.IO) { val preferences: PreferencesHelper by injectLazy() try { - val pendingUpdates = ExtensionGithubApi().checkforUpdates(this@MainActivity) + val pendingUpdates = ExtensionGithubApi().checkForUpdates(this@MainActivity) preferences.extensionUpdatesCount().set(pendingUpdates.size) preferences.lastExtCheck().set(Date().time) - } catch (e: java.lang.Exception) { } + } catch (e: java.lang.Exception) { + } } } } @@ -334,25 +359,44 @@ open class MainActivity : BaseActivity() { applicationContext, notificationId, intent.getIntExtra("groupId", 0) ) when (intent.action) { - SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) - SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) - SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) - SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) - SHORTCUT_EXTENSIONS -> setSelectedDrawerItem(R.id.nav_drawer_extensions) + SHORTCUT_LIBRARY -> bottom_nav.selectedItemId = R.id.nav_library + SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ -> { + bottom_nav.selectedItemId = R.id.nav_recents + val controller: Controller = when (intent.action) { + SHORTCUT_RECENTLY_UPDATED -> RecentChaptersController() + else -> RecentlyReadController() + } + router.pushController(controller.withFadeTransaction()) + } + SHORTCUT_CATALOGUES -> bottom_nav.selectedItemId = R.id.nav_browse + SHORTCUT_EXTENSIONS -> { + bottom_nav.selectedItemId = R.id.nav_browse + router.popToRoot() + bottom_nav.post { + val controller = + router.backstack.firstOrNull()?.controller() as? CatalogueController + controller?.showSheet() + } + } SHORTCUT_MANGA -> { val extras = intent.extras ?: return false - router.setRoot(RouterTransaction.with(MangaController(extras))) + if (router.backstack.isEmpty()) bottom_nav.selectedItemId = R.id.nav_library + router.pushController(MangaDetailsController(extras).withFadeTransaction()) } SHORTCUT_DOWNLOADS -> { - if (router.backstack.none { it.controller() is DownloadController }) { - setSelectedDrawerItem(R.id.nav_drawer_downloads) + bottom_nav.selectedItemId = R.id.nav_recents + router.popToRoot() + bottom_nav.post { + val controller = + router.backstack.firstOrNull()?.controller() as? RecentsController + controller?.showSheet() } } Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> { - //If the intent match the "standard" Android search intent + // If the intent match the "standard" Android search intent // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) - //Get the search query provided in extras, and if not null, perform a global search with it. + // Get the search query provided in extras, and if not null, perform a global search with it. val query = intent.getStringExtra(SearchManager.QUERY) if (query != null && query.isNotEmpty()) { if (router.backstackSize > 1) { @@ -368,7 +412,12 @@ open class MainActivity : BaseActivity() { if (router.backstackSize > 1) { router.popToRoot() } - router.pushController(CatalogueSearchController(query, filter).withFadeTransaction()) + router.pushController( + CatalogueSearchController( + query, + filter + ).withFadeTransaction() + ) } } else -> return false @@ -378,56 +427,60 @@ open class MainActivity : BaseActivity() { override fun onDestroy() { super.onDestroy() - nav_view?.setNavigationItemSelectedListener(null) + DownloadService.removeListener(this) toolbar?.setNavigationOnClickListener(null) } override fun onBackPressed() { - if (trulyGoBack) { - super.onBackPressed() - return - } - val backstackSize = router.backstackSize - if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { - drawer.closeDrawers() - } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { - setSelectedDrawerItem(startScreenId) - } else if (!router.handleBack()) { - unlocked = false + val sheetController = router.backstack.last().controller() as? BottomSheetController + if (if (router.backstackSize == 1) !(sheetController?.handleSheetBack() ?: false) + else !router.handleBack() + ) { + SecureActivityDelegate.locked = true super.onBackPressed() } } - fun setSelectedDrawerItem(itemId: Int) { - if (!isFinishing) { - nav_view.setCheckedItem(itemId) - nav_view.menu.performIdentifierAction(itemId, 0) - } - } - private fun setRoot(controller: Controller, id: Int) { router.setRoot(controller.withFadeTransaction().tag(id.toString())) } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + // Initialize option to open catalogue settings. + R.id.action_settings -> { + router.pushController( + (RouterTransaction.with(SettingsMainController())).popChangeHandler( + FadeChangeHandler() + ).pushChangeHandler(FadeChangeHandler()) + ) + } + else -> return super.onOptionsItemSelected(item) + } + return super.onOptionsItemSelected(item) + } + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + gestureDetector.onTouchEvent(ev) if (ev?.action == MotionEvent.ACTION_DOWN) { if (snackBar != null && snackBar!!.isShown) { val sRect = Rect() snackBar!!.view.getGlobalVisibleRect(sRect) - val extRect:Rect? = if (extraViewForUndo != null) Rect() else null + val extRect: Rect? = if (extraViewForUndo != null) Rect() else null extraViewForUndo?.getGlobalVisibleRect(extRect) - //This way the snackbar will only be dismissed if - //the user clicks outside it. - if (canDismissSnackBar && !sRect.contains(ev.x.toInt(), ev.y.toInt()) - && (extRect == null || - !extRect.contains(ev.x.toInt(), ev.y.toInt()))) { + // This way the snackbar will only be dismissed if + // the user clicks outside it. + if (canDismissSnackBar && !sRect.contains( + ev.x.toInt(), + ev.y.toInt() + ) && (extRect == null || !extRect.contains(ev.x.toInt(), ev.y.toInt())) + ) { snackBar?.dismiss() snackBar = null extraViewForUndo = null } - } - else if (snackBar != null) { + } else if (snackBar != null) { snackBar = null extraViewForUndo = null } @@ -435,54 +488,102 @@ open class MainActivity : BaseActivity() { return super.dispatchTouchEvent(ev) } - protected open fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { + protected open fun syncActivityViewWithController( + to: Controller?, + from: Controller? = null, + isPush: Boolean = false + ) { if (from is DialogController || to is DialogController) { return } - - val showHamburger = router.backstackSize == 1 - if (showHamburger) { - drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED) + val onRoot = router.backstackSize == 1 + if (onRoot) { + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + toolbar.navigationIcon = searchDrawable } else { - drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + toolbar.navigationIcon = drawerArrow } + drawerArrow?.progress = 1f + + if (to !is DialogController) { + bottom_nav.visibility = + if (router.backstackSize == 0 || (router.backstackSize <= 1 && !isPush)) View.VISIBLE else bottom_nav.visibility + animationSet?.cancel() + animationSet = AnimatorSet() + val alphaAnimation = ValueAnimator.ofFloat( + bottom_nav.alpha, if (router.backstackSize > 1) 0f else 1f + ) + alphaAnimation.addUpdateListener { valueAnimator -> + bottom_nav.alpha = valueAnimator.animatedValue as Float + } + alphaAnimation.addListener(object : Animator.AnimatorListener { + override fun onAnimationEnd(animation: Animator?) { + bottom_nav.visibility = + if (router.backstackSize > 1) View.GONE else View.VISIBLE + } - ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start() + override fun onAnimationCancel(animation: Animator?) {} - if (from is TabbedController) { - from.cleanupTabs(tabs) - } - if (to is TabbedController) { - tabAnimator.expand() - to.configureTabs(tabs) - } else { - tabAnimator.collapse() - tabs.setupWithViewPager(null) + override fun onAnimationRepeat(animation: Animator?) {} + + override fun onAnimationStart(animation: Animator?) {} + }) + alphaAnimation.duration = 200 + alphaAnimation.startDelay = 50 + animationSet?.playTogether(alphaAnimation) + animationSet?.start() } + } - if (from is SecondaryDrawerController) { - if (secondaryDrawer != null) { - from.cleanupSecondaryDrawer(drawer) - drawer.removeView(secondaryDrawer) - secondaryDrawer = null + override fun downloadStatusChanged(downloading: Boolean) { + val downloadManager = Injekt.get() + val hasQueue = downloading || downloadManager.hasQueue() + launchUI { + if (hasQueue) { + bottom_nav?.getOrCreateBadge(R.id.nav_recents) + } else { + bottom_nav?.removeBadge(R.id.nav_recents) } } - if (to is SecondaryDrawerController) { - val newDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) } - secondaryDrawer = if (newDrawer == null && secondaryDrawer != null) { - drawer.removeView(secondaryDrawer) - null - } else newDrawer + } + + private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent): Boolean { + return true } - if (to is NoToolbarElevationController) { - appbar.disableElevation() - } else { - appbar.enableElevation() + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + var result = false + val diffY = e2.y - e1.y + val diffX = e2.x - e1.x + if (abs(diffX) <= abs(diffY)) { + val sheetRect = Rect() + bottom_nav.getGlobalVisibleRect(sheetRect) + if (sheetRect.contains( + e1.x.toInt(), e1.y.toInt() + ) && abs(diffY) > Companion.SWIPE_THRESHOLD && abs(velocityY) > Companion.SWIPE_VELOCITY_THRESHOLD && diffY <= 0 + ) { + val bottomSheetController = + router.backstack.lastOrNull()?.controller() as? BottomSheetController + bottomSheetController?.showSheet() + } + result = true + } + return result } } companion object { + + private const val SWIPE_THRESHOLD = 100 + private const val SWIPE_VELOCITY_THRESHOLD = 100 + // Shortcut actions const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" @@ -495,10 +596,22 @@ open class MainActivity : BaseActivity() { const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" const val INTENT_SEARCH_QUERY = "query" const val INTENT_SEARCH_FILTER = "filter" + } +} - private const val URL_HELP = "https://tachiyomi.org/help/" +interface BottomNavBarInterface { + fun canChangeTabs(block: () -> Unit): Boolean +} - var unlocked = false +interface RootSearchInterface { + fun expandSearch() { + if (this is Controller) activity?.toolbar?.menu?.findItem(R.id.action_search) + ?.expandActionView() } +} +interface BottomSheetController { + fun showSheet() + fun toggleSheet() + fun handleSheetBack(): Boolean } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt index db0054b158..b25034f5ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt @@ -1,175 +1,57 @@ package eu.kanade.tachiyomi.ui.main import android.app.SearchManager -import android.content.Context import android.content.Intent -import android.content.res.Configuration -import android.graphics.Color -import android.os.Build import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.LinearLayout -import androidx.appcompat.graphics.drawable.DrawerArrowDrawable -import androidx.core.graphics.ColorUtils -import com.bluelinelabs.conductor.Conductor import com.bluelinelabs.conductor.Controller -import com.bluelinelabs.conductor.ControllerChangeHandler -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePadding -import kotlinx.android.synthetic.main.search_activity.* +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate +import eu.kanade.tachiyomi.util.view.gone +import kotlinx.android.synthetic.main.main_activity.* -class SearchActivity: MainActivity() { - override var trulyGoBack = true +class SearchActivity : MainActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - setContentView(R.layout.search_activity) - - setSupportActionBar(sToolbar) - - drawerArrow = DrawerArrowDrawable(this) - drawerArrow?.color = Color.WHITE - sToolbar.navigationIcon = drawerArrow - - tabAnimator = TabsAnimator(sTabs) - - val container: ViewGroup = findViewById(R.id.controller_container) - - val content: LinearLayout = findViewById(R.id.main_content) - container.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - - content.setOnApplyWindowInsetsListener { v, insets -> - window.navigationBarColor = - // if the os does not support light nav bar and is portrait, draw a dark translucent - // nav bar - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - (v.rootWindowInsets.systemWindowInsetLeft > 0 || - v.rootWindowInsets.systemWindowInsetRight > 0)) - // For lollipop, draw opaque nav bar - Color.BLACK - else Color.argb(179, 0, 0, 0) - } - // if the android q+ device has gesture nav, transparent nav bar - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && (v.rootWindowInsets.systemWindowInsetBottom != v.rootWindowInsets - .tappableElementInsets.bottom)) { - getColor(android.R.color.transparent) - } - // if in landscape with 2/3 button mode, fully opaque nav bar - else if (v.rootWindowInsets.systemWindowInsetLeft > 0 - || v.rootWindowInsets.systemWindowInsetRight > 0) { - getResourceColor(android.R.attr.colorBackground) - } - // if in portrait with 2/3 button mode, translucent nav bar - else { - ColorUtils.setAlphaComponent( - getResourceColor(android.R.attr.colorBackground), 179) - } - v.setPadding(insets.systemWindowInsetLeft, insets.systemWindowInsetTop, - insets.systemWindowInsetRight, 0) - insets - } - val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - if (Build.VERSION.SDK_INT >= 26 && currentNightMode == Configuration.UI_MODE_NIGHT_NO) { - content.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - } - - val drawerContainer: FrameLayout = findViewById(R.id.search_container) - drawerContainer.setOnApplyWindowInsetsListener { v, insets -> - window.statusBarColor = getResourceColor(R.attr.colorPrimary) - val contextView = window?.decorView?.findViewById(R.id.action_mode_bar) - contextView?.updateLayoutParams { - leftMargin = insets.systemWindowInsetLeft - rightMargin = insets.systemWindowInsetRight - } - // Consume any horizontal insets and pad all content in. There's not much we can do - // with horizontal insets - v.updatePadding( - left = insets.systemWindowInsetLeft, - right = insets.systemWindowInsetRight - ) - insets.replaceSystemWindowInsets( - 0, insets.systemWindowInsetTop, - 0, insets.systemWindowInsetBottom - ) - } - - router = Conductor.attachRouter(this, container, savedInstanceState) - if (!router.hasRootController()) { - // Set start screen - handleIntentAction(intent) - } - - sToolbar.setNavigationOnClickListener { + toolbar.navigationIcon = drawerArrow + toolbar.setNavigationOnClickListener { popToRoot() } + } - router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { - override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean, - container: ViewGroup, handler: ControllerChangeHandler - ) { - - syncActivityViewWithController(to, from) - } - - override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean, - container: ViewGroup, handler: ControllerChangeHandler - ) { - - } - - }) - - syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) + override fun onBackPressed() { + if (router.backstack.size <= 1 || !router.handleBack()) { + SecureActivityDelegate.locked = true + super.onBackPressed() + } } - private fun Context.popToRoot() { - val intent = Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK + private fun popToRoot() { + if (!router.handleBack()) { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + finishAfterTransition() } - startActivity(intent) - finishAfterTransition() } - override fun syncActivityViewWithController(to: Controller?, from: Controller?) { + override fun syncActivityViewWithController( + to: Controller?, + from: Controller?, + isPush: + Boolean + ) { if (from is DialogController || to is DialogController) { return } + toolbar.navigationIcon = drawerArrow drawerArrow?.progress = 1f - if (from is TabbedController) { - from.cleanupTabs(sTabs) - } - if (to is TabbedController) { - tabAnimator.expand() - to.configureTabs(sTabs) - } else { - tabAnimator.collapse() - sTabs.setupWithViewPager(null) - } - - if (to is NoToolbarElevationController) { - appbar.disableElevation() - } else { - appbar.enableElevation() - } + bottom_nav.gone() } override fun handleIntentAction(intent: Intent): Boolean { @@ -179,10 +61,10 @@ class SearchActivity: MainActivity() { ) when (intent.action) { Intent.ACTION_SEARCH, "com.google.android.gms.actions.SEARCH_ACTION" -> { - //If the intent match the "standard" Android search intent + // If the intent match the "standard" Android search intent // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) - //Get the search query provided in extras, and if not null, perform a global search with it. + // Get the search query provided in extras, and if not null, perform a global search with it. val query = intent.getStringExtra(SearchManager.QUERY) if (query != null && query.isNotEmpty()) { router.replaceTopController(CatalogueSearchController(query).withFadeTransaction()) @@ -202,4 +84,4 @@ class SearchActivity: MainActivity() { } return true } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt index a4014beeda..78f3f37086 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.ui.main import android.animation.ObjectAnimator -import com.google.android.material.tabs.TabLayout import android.view.ViewTreeObserver import android.view.animation.DecelerateInterpolator +import com.google.android.material.tabs.TabLayout class TabsAnimator(val tabs: TabLayout) { @@ -102,5 +102,4 @@ class TabsAnimator(val tabs: TabLayout) { */ private val isMeasured: Boolean get() = tabsHeight > 0 - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt new file mode 100644 index 0000000000..642293e6f3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChooseShapeDialog.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItemsSingleChoice +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +/** + * Dialog to choose a shape for the icon. + */ +class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) { + + constructor(target: MangaDetailsController) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val modes = intArrayOf( + R.string.circular, + R.string.rounded, + R.string.square, + R.string.star) + + return MaterialDialog(activity!!) + .title(R.string.icon_shape) + .negativeButton(android.R.string.cancel) + .listItemsSingleChoice( + items = modes.map { activity?.getString(it) as CharSequence }, + waitForPositiveButton = false) { _, i, _ -> + (targetController as? MangaDetailsController)?.createShortcutForShape(i) + dismissDialog() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt new file mode 100644 index 0000000000..922732f5bb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -0,0 +1,115 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.app.Dialog +import android.net.Uri +import android.os.Bundle +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.customview.customView +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.signature.ObjectKey +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import kotlinx.android.synthetic.main.edit_manga_dialog.view.* +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class EditMangaDialog : DialogController { + + private var dialogView: View? = null + + private val manga: Manga + + private var customCoverUri: Uri? = null + + private val infoController + get() = targetController as MangaDetailsController + + constructor(target: MangaDetailsController, manga: Manga) : super(Bundle() + .apply { + putLong(KEY_MANGA, manga.id!!) + }) { + targetController = target + this.manga = manga + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + manga = Injekt.get().getManga(bundle.getLong(KEY_MANGA)) + .executeAsBlocking()!! + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val dialog = MaterialDialog(activity!!).apply { + customView(viewRes = R.layout.edit_manga_dialog, scrollable = true) + negativeButton(android.R.string.cancel) + positiveButton(R.string.save) { onPositiveButtonClick() } + } + dialogView = dialog.view + onViewCreated(dialog.view) + dialog.setOnShowListener { + val dView = (it as? MaterialDialog)?.view + dView?.contentLayout?.scrollView?.scrollTo(0, 0) + } + return dialog + } + + fun onViewCreated(view: View) { + GlideApp.with(view.context) + .asDrawable() + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) + .dontAnimate() + .into(view.manga_cover) + val isLocal = manga.source == LocalSource.ID + + if (isLocal) { + if (manga.title != manga.url) + view.manga_full_title.append(manga.title) + view.manga_full_title.hint = "${resources?.getString(R.string.title)}: ${manga.url}" + view.manga_author.append(manga.author ?: "") + view.manga_artist.append(manga.artist ?: "") + view.manga_description.append(manga.description ?: "") + view.manga_genres_tags.setTags(manga.genre?.split(", ") ?: emptyList()) + } + view.manga_genres_tags.clearFocus() + view.cover_layout.setOnClickListener { + infoController.changeCover() + } + view.reset_tags.setOnClickListener { resetTags() } + } + + private fun resetTags() { + if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setTags( + emptyList() + ) + else dialogView?.manga_genres_tags?.setTags(manga.genre?.split(", ")) + } + + fun updateCover(uri: Uri) { + GlideApp.with(dialogView!!.context).load(uri).into(dialogView!!.manga_cover) + customCoverUri = uri + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + dialogView = null + } + + private fun onPositiveButtonClick() { + infoController.presenter.updateManga(dialogView?.manga_full_title?.text.toString(), + dialogView?.manga_author?.text.toString(), dialogView?.manga_artist?.text.toString(), + customCoverUri, dialogView?.manga_description?.text.toString(), + dialogView?.manga_genres_tags?.tags) + } + + private companion object { + const val KEY_MANGA = "manga_id" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt deleted file mode 100644 index d695df62e0..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ /dev/null @@ -1,252 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import com.bluelinelabs.conductor.Router -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.support.RouterPagerAdapter -import com.google.android.material.tabs.TabLayout -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.notification.NotificationReceiver -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.RxController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController -import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe -import eu.kanade.tachiyomi.ui.catalogue.CatalogueController -import eu.kanade.tachiyomi.ui.main.SearchActivity -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController -import eu.kanade.tachiyomi.ui.manga.track.TrackController -import kotlinx.android.synthetic.main.search_activity.sTabs -import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.main_activity.tabs -import kotlinx.android.synthetic.main.manga_controller.manga_pager -import rx.Subscription -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -class MangaController : RxController, TabbedController { - - constructor(manga: Manga?, - fromCatalogue: Boolean = false, - smartSearchConfig: CatalogueController.SmartSearchConfig? = null, - update: Boolean = false) : super(Bundle().apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) - putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) - putBoolean(UPDATE_EXTRA, update) - }) { - this.manga = manga - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(manga: Manga?, fromCatalogue: Boolean = false, fromExtension: Boolean = false) : - super - (Bundle() - .apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) - }) { - this.manga = manga - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(manga: Manga?, startY:Float?) : super(Bundle().apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_CATALOGUE_EXTRA, false) - }) { - this.manga = manga - startingChapterYPos = startY - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - constructor(mangaId: Long) : this( - Injekt.get().getManga(mangaId).executeAsBlocking()) - - constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) { - val notificationId = bundle.getInt("notificationId", -1) - val context = applicationContext ?: return - if (notificationId > -1) NotificationReceiver.dismissNotification( - context, notificationId, bundle.getInt("groupId", 0) - ) - } - - var manga: Manga? = null - private set - - var source: Source? = null - private set - - var startingChapterYPos:Float? = null - - private var adapter: MangaDetailAdapter? = null - - val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) - - val lastUpdateRelay: BehaviorRelay = BehaviorRelay.create() - - val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() - - val mangaFavoriteRelay: PublishRelay = PublishRelay.create() - - private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() - - private var trackingIconSubscription: Subscription? = null - - override fun getTitle(): String? { - return manga?.currentTitle() - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.manga_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - if (manga == null || source == null) return - - requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) - - adapter = MangaDetailAdapter() - manga_pager.offscreenPageLimit = 3 - manga_pager.adapter = adapter - - if (!fromCatalogue) - manga_pager.currentItem = CHAPTERS_CONTROLLER - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - adapter = null - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (type.isEnter) { - tabLayout()?.setupWithViewPager(manga_pager) - checkInitialTrackState() - trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } - } - } - - private fun checkInitialTrackState() { - val manga = manga ?: return - val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } - val db = Injekt.get() - val tracks = db.getTracks(manga).executeAsBlocking() - - if (loggedServices.any { service -> tracks.any { it.sync_id == service.id } }) { - setTrackingIcon(true) - } - } - - fun tabLayout():TabLayout? { - return if (activity is SearchActivity) activity?.sTabs - else activity?.tabs - } - - fun updateTitle(manga: Manga) { - this.manga?.title = manga.title - setTitle() - } - - override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeEnded(handler, type) - if (manga == null || source == null) { - activity?.toast(R.string.manga_not_in_db) - router.popController(this) - } - } - - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_FILL - tabMode = TabLayout.MODE_FIXED - } - } - - override fun cleanupTabs(tabs: TabLayout) { - trackingIconSubscription?.unsubscribe() - setTrackingIconInternal(false) - } - - fun setTrackingIcon(visible: Boolean) { - trackingIconRelay.call(visible) - } - - private fun setTrackingIconInternal(visible: Boolean) { - val tab = tabLayout()?.getTabAt(TRACK_CONTROLLER) ?: return - val drawable = if (visible) - VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) - else null - - tab.icon = drawable - } - - private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { - - private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 - - private val tabTitles = listOf( - R.string.manga_detail_tab, - R.string.manga_chapters_tab, - R.string.manga_tracking_tab) - .map { resources!!.getString(it) } - - override fun getCount(): Int { - return tabCount - } - - override fun configureRouter(router: Router, position: Int) { - val touchOffset = if (tabLayout()?.height == 0) 144f else 0f - if (!router.hasRootController()) { - val controller = when (position) { - INFO_CONTROLLER -> MangaInfoController() - CHAPTERS_CONTROLLER -> ChaptersController(startingChapterYPos?.minus(touchOffset)) - TRACK_CONTROLLER -> TrackController() - else -> error("Wrong position $position") - } - router.setRoot(RouterTransaction.with(controller)) - } - } - - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] - } - - } - - companion object { - - const val UPDATE_EXTRA = "update" - const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig" - - const val FROM_CATALOGUE_EXTRA = "from_catalogue" - const val MANGA_EXTRA = "manga" - - const val INFO_CONTROLLER = 0 - const val CHAPTERS_CONTROLLER = 1 - const val TRACK_CONTROLLER = 2 - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt new file mode 100644 index 0000000000..901d0041c5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt @@ -0,0 +1,145 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.ItemTouchHelper +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.util.system.getResourceColor +import uy.kohesive.injekt.injectLazy +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + +class MangaDetailsAdapter( + val controller: MangaDetailsController, + context: Context +) : BaseChapterAdapter>(controller) { + + val preferences: PreferencesHelper by injectLazy() + + var items: List = emptyList() + + val delegate: MangaDetailsInterface = controller + val presenter = controller.presenter + + val readColor = context.getResourceColor(android.R.attr.textColorHint) + + val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary) + + val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) + + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + fun setChapters(items: List?) { + this.items = items ?: emptyList() + performFilter() + } + + fun indexOf(item: ChapterItem): Int { + return items.indexOf(item) + } + + fun indexOf(chapterId: Long): Int { + return currentItems.indexOfFirst { it is ChapterItem && it.id == chapterId } + } + + fun performFilter() { + val s = getFilter(String::class.java) + if (s.isNullOrBlank()) { + updateDataSet(items) + } else { + updateDataSet(items.filter { it.name.contains(s, true) || + it.scanlator?.contains(s, true) == true }) + } + } + + override fun onItemSwiped(position: Int, direction: Int) { + super.onItemSwiped(position, direction) + when (direction) { + ItemTouchHelper.RIGHT -> controller.bookmarkChapter(position) + ItemTouchHelper.LEFT -> controller.toggleReadChapter(position) + } + } + + fun getSectionText(position: Int): String? { + val chapter = getItem(position) as? ChapterItem ?: return null + if (position == itemCount - 1) return "-" + return when (presenter.scrollType) { + MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> + presenter.getGroupNumber(chapter)?.toString() ?: "*" + MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS -> + if (chapter.chapter_number < 0) "*" + else (chapter.chapter_number / 100).toInt().toString() + MangaDetailsPresenter.TENS_OF_CHAPTERS -> + if (chapter.chapter_number < 0) "*" + else (chapter.chapter_number / 10).toInt().toString() + else -> null + } + } + + fun getFullText(position: Int): String { + val chapter = + getItem(position) as? ChapterItem ?: return recyclerView.context.getString(R.string.top) + if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom) + return when (val scrollType = presenter.scrollType) { + MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> { + val volume = presenter.getGroupNumber(chapter) + if (volume != null) recyclerView.context.getString( + if (scrollType == MangaDetailsPresenter.MULTIPLE_SEASONS) R.string.season_ + else R.string.volume_, volume) + else recyclerView.context.getString(R.string.unknown) + } + MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS -> recyclerView.context.getString( + R.string.chapters_, get100sRange( + chapter.chapter_number + ) + ) + MangaDetailsPresenter.TENS_OF_CHAPTERS -> recyclerView.context.getString( + R.string.chapters_, get10sRange( + chapter.chapter_number + ) + ) + else -> recyclerView.context.getString(R.string.unknown) + } + } + + private fun get100sRange(value: Float): String { + val number = value.toInt() + return if (number < 100) "0-99" + else { + val hundred = number / 100 + "${hundred}00-${hundred}99" + } + } + + private fun get10sRange(value: Float): String { + val number = value.toInt() + return if (number < 10) "0-9" + else { + val hundred = number / 10 + "${hundred}0-${hundred + 1}9" + } + } + + interface MangaDetailsInterface : MangaHeaderInterface, DownloadInterface + + interface MangaHeaderInterface { + fun coverColor(): Int? + fun mangaPresenter(): MangaDetailsPresenter + fun prepareToShareManga() + fun openInWebView() + fun startDownloadRange(position: Int) + fun readNextChapter() + fun topCoverHeight(): Int + fun tagClicked(text: String) + fun showChapterFilter() + fun favoriteManga(longPress: Boolean) + fun copyToClipboard(content: String, label: Int) + fun zoomImageFromThumb(thumbView: View) + fun showTrackingSheet() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt new file mode 100644 index 0000000000..60f5b8a936 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -0,0 +1,1560 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.app.Activity +import android.app.PendingIntent +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.ViewPropertyAnimator +import android.view.animation.DecelerateInterpolator +import android.view.inputmethod.InputMethodManager +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.IconCompat +import androidx.core.math.MathUtils +import androidx.palette.graphics.Palette +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.ChangeBounds +import androidx.transition.ChangeImageTransform +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItems +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.bumptech.glide.signature.ObjectKey +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import com.reddit.indicatorfastscroll.FastScrollItemIndicator +import com.reddit.indicatorfastscroll.FastScrollerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.controller.BaseController +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.main.SearchActivity +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterHolder +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersSortBottomSheet +import eu.kanade.tachiyomi.ui.manga.track.TrackItem +import eu.kanade.tachiyomi.ui.manga.track.TrackingBottomSheet +import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate +import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.storage.getUriCompat +import eu.kanade.tachiyomi.util.system.ThemeUtil +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.system.pxToDp +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets +import eu.kanade.tachiyomi.util.view.getText +import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener +import eu.kanade.tachiyomi.util.view.setStyle +import eu.kanade.tachiyomi.util.view.snack +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.updatePaddingRelative +import jp.wasabeef.glide.transformations.CropSquareTransformation +import jp.wasabeef.glide.transformations.MaskTransformation +import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.manga_details_controller.* +import kotlinx.android.synthetic.main.manga_header_item.* +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.io.IOException +import java.util.Locale +import kotlin.math.abs +import kotlin.math.max + +class MangaDetailsController : BaseController, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ActionMode.Callback, + MangaDetailsAdapter.MangaDetailsInterface, + FlexibleAdapter.OnItemMoveListener, + ChangeMangaCategoriesDialog.Listener { + + constructor( + manga: Manga?, + fromCatalogue: Boolean = false, + smartSearchConfig: CatalogueController.SmartSearchConfig? = null, + update: Boolean = false + ) : super(Bundle().apply { + putLong(MANGA_EXTRA, manga?.id ?: 0) + putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) + putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) + putBoolean(UPDATE_EXTRA, update) + }) { + this.manga = manga + if (manga != null) { + source = Injekt.get().getOrStub(manga.source) + } + presenter = MangaDetailsPresenter(this, manga!!, source!!) + } + + constructor(mangaId: Long) : this( + Injekt.get().getManga(mangaId).executeAsBlocking()) + + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) { + val notificationId = bundle.getInt("notificationId", -1) + val context = applicationContext ?: return + if (notificationId > -1) NotificationReceiver.dismissNotification( + context, notificationId, bundle.getInt("groupId", 0) + ) + } + + private var manga: Manga? = null + private var source: Source? = null + var colorAnimator: ValueAnimator? = null + val presenter: MangaDetailsPresenter + var coverColor: Int? = null + var toolbarIsColored = false + private var snack: Snackbar? = null + val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) + var coverDrawable: Drawable? = null + private var trackingBottomSheet: TrackingBottomSheet? = null + private var startingDLChapterPos: Int? = null + private var editMangaDialog: EditMangaDialog? = null + var refreshTracker: Int? = null + private var textAnim: ViewPropertyAnimator? = null + private var scrollAnim: ViewPropertyAnimator? = null + var isTablet = false + var chapterPopupMenu: Pair? = null + + // Tablet Layout + var tabletRecycler: RecyclerView? = null + + /** + * Adapter containing a list of chapters. + */ + private var tabletAdapter: MangaDetailsAdapter? = null + + /** + * Library search query. + */ + private var query = "" + + /** + * Adapter containing a list of chapters. + */ + private var adapter: MangaDetailsAdapter? = null + + /** + * Action mode for selections. + */ + private var actionMode: ActionMode? = null + + // Hold a reference to the current animator, so that it can be canceled mid-way. + private var currentAnimator: Animator? = null + + var showScroll = false + var headerHeight = 0 + + init { + setHasOptionsMenu(true) + } + + override fun getTitle(): String? { + return if (toolbarIsColored && !isTablet) manga?.title else null + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + coverColor = null + + // Init RecyclerView and adapter + adapter = MangaDetailsAdapter(this, view.context) + isTablet = isTabletSize() + if (isTablet) { + tabletRecycler = RecyclerView(view.context) + linear_recycler_layout.addView(tabletRecycler, 0) + tabletRecycler?.updateLayoutParams { + weight = 0.4f + height = ViewGroup.LayoutParams.MATCH_PARENT + width = ViewGroup.LayoutParams.MATCH_PARENT + } + tabletRecycler?.clipToPadding = false + tabletAdapter = MangaDetailsAdapter(this, view.context) + tabletRecycler?.adapter = tabletAdapter + tabletRecycler?.layoutManager = LinearLayoutManager(view.context) + val divider = View(view.context) + divider.setBackgroundColor(ContextCompat.getColor(view.context, R.color.divider)) + linear_recycler_layout.addView(divider, 1) + divider.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + width = 1.dpToPx + } + } + + recycler.adapter = adapter + adapter?.isSwipeEnabled = true + recycler.layoutManager = LinearLayoutManager(view.context) + recycler.addItemDecoration( + DividerItemDecoration( + view.context, + DividerItemDecoration.VERTICAL + ) + ) + recycler.setHasFixedSize(true) + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = view.context.obtainStyledAttributes(attrsArray) + val appbarHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + val offset = 10.dpToPx + var statusBarHeight = -1 + swipe_refresh.setStyle() + swipe_refresh.setDistanceToTriggerSync(70.dpToPx) + activity!!.appbar.elevation = 0f + + recycler.doOnApplyWindowInsets { v, insets, _ -> + v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + tabletRecycler?.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + headerHeight = appbarHeight + insets.systemWindowInsetTop + statusBarHeight = insets.systemWindowInsetTop + swipe_refresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset) + if (isTablet) v.updatePaddingRelative(top = headerHeight + 1.dpToPx) + getHeader()?.setTopHeight(headerHeight) + fast_scroll_layout.updateLayoutParams { + topMargin = headerHeight + bottomMargin = insets.systemWindowInsetBottom + } + v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + } + + presenter.onCreate() + fast_scroller.translationX = if (showScroll || isTablet) 0f else 25f.dpToPx + recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val atTop = !recycler.canScrollVertically(-1) + if (!isTablet) { + val tY = getHeader()?.backdrop?.translationY ?: 0f + getHeader()?.backdrop?.translationY = max(0f, tY + dy * 0.25f) + if (router?.backstack?.lastOrNull() + ?.controller() == this@MangaDetailsController && statusBarHeight > -1 && activity != null && activity!!.appbar.height > 0 + ) { + activity!!.appbar.y -= dy + activity!!.appbar.y = MathUtils.clamp( + activity!!.appbar.y, -activity!!.appbar.height.toFloat(), 0f + ) + } + val appBarY = activity?.appbar?.y ?: 0f + if ((!atTop && !toolbarIsColored && (appBarY < (-headerHeight + 1) || (dy < 0 && appBarY == 0f))) || (atTop && toolbarIsColored)) { + colorToolbar(!atTop) + } + } else { + if ((!atTop && !toolbarIsColored) || (atTop && toolbarIsColored)) { + colorToolbar(!atTop) + } + } + if (atTop) { + getHeader()?.backdrop?.translationY = 0f + activity!!.appbar.y = 0f + } + if (!isTablet) { + val fPosition = + (recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (fPosition > 0 && !showScroll) { + showScroll = true + scrollAnim?.cancel() + scrollAnim = fast_scroller.animate().setDuration(100).translationX(0f) + scrollAnim?.start() + } else if (fPosition <= 0 && showScroll) { + showScroll = false + scrollAnim?.cancel() + scrollAnim = + fast_scroller.animate().setDuration(100).translationX(25f.dpToPx) + scrollAnim?.start() + } + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + val atTop = !recycler.canScrollVertically(-1) + if (newState == RecyclerView.SCROLL_STATE_IDLE && !isTablet) { + if (router?.backstack?.lastOrNull() + ?.controller() == this@MangaDetailsController && statusBarHeight > -1 && activity != null && + activity!!.appbar.height > 0 + ) { + val halfWay = abs((-activity!!.appbar.height.toFloat()) / 2) + val shortAnimationDuration = resources?.getInteger( + android.R.integer.config_shortAnimTime + ) ?: 0 + val closerToTop = abs(activity!!.appbar.y) - halfWay > 0 + activity!!.appbar.animate().y( + if (closerToTop && !atTop) (-activity!!.appbar.height.toFloat()) + else 0f + ).setDuration(shortAnimationDuration.toLong()).start() + if (!closerToTop && !atTop && !toolbarIsColored) + colorToolbar(true) + } + } + if (atTop && toolbarIsColored) colorToolbar(false) + if (atTop) { + getHeader()?.backdrop?.translationY = 0f + activity!!.appbar.y = 0f + } + } + }) + setPaletteColor() + + fast_scroller.setupWithRecyclerView(recycler, { position -> + val letter = adapter?.getSectionText(position) + when { + presenter.scrollType == 0 -> null + letter != null -> FastScrollItemIndicator.Text(letter) + else -> FastScrollItemIndicator.Icon(R.drawable.ic_star_24dp) + } + }) + fast_scroller.useDefaultScroller = false + fast_scroller.itemIndicatorSelectedCallbacks += object : + FastScrollerView.ItemIndicatorSelectedCallback { + override fun onItemIndicatorSelected( + indicator: FastScrollItemIndicator, + indicatorCenterY: Int, + itemPosition: Int + ) { + textAnim?.cancel() + textAnim = text_view_m.animate().alpha(0f).setDuration(250L).setStartDelay(1000) + textAnim?.start() + + text_view_m.translationY = indicatorCenterY.toFloat() - text_view_m.height / 2 + text_view_m.alpha = 1f + text_view_m.text = adapter?.getFullText(itemPosition) + val appbar = activity?.appbar + appbar?.y = 0f + (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + itemPosition, headerHeight + ) + colorToolbar(itemPosition > 0, false) + } + } + + swipe_refresh.isRefreshing = presenter.isLoading + if (manga?.initialized != true) + swipe_refresh.post { swipe_refresh.isRefreshing = true } + + swipe_refresh.setOnRefreshListener { presenter.refreshAll() } + } + + fun colorToolbar(isColor: Boolean, animate: Boolean = true) { + if (isColor == toolbarIsColored) return + toolbarIsColored = isColor + val isCurrentController = + router?.backstack?.lastOrNull()?.controller() == this@MangaDetailsController + if (isCurrentController) setTitle() + if (actionMode != null) { + (activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT) + return + } + val color = + coverColor ?: activity!!.getResourceColor(R.attr.colorPrimaryVariant) + val colorFrom = + if (colorAnimator?.isRunning == true) activity?.window?.statusBarColor + ?: color + else ColorUtils.setAlphaComponent( + color, if (toolbarIsColored) 0 else 175 + ) + val colorTo = ColorUtils.setAlphaComponent( + color, if (toolbarIsColored) 175 else 0 + ) + colorAnimator?.cancel() + if (animate) { + colorAnimator = ValueAnimator.ofObject( + android.animation.ArgbEvaluator(), colorFrom, colorTo + ) + colorAnimator?.duration = 250 // milliseconds + colorAnimator?.addUpdateListener { animator -> + (activity as MainActivity).toolbar.setBackgroundColor(animator.animatedValue as Int) + activity?.window?.statusBarColor = (animator.animatedValue as Int) + } + colorAnimator?.start() + } else { + (activity as MainActivity).toolbar.setBackgroundColor(colorTo) + activity?.window?.statusBarColor = colorTo + } + } + + fun setPaletteColor() { + val view = view ?: return + GlideApp.with(view.context).load(manga) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga!!.id!!).toString())) + .into(object : CustomTarget() { + override fun onResourceReady( + resource: Drawable, + transition: Transition? + ) { + coverDrawable = resource + val bitmapCover = resource as? BitmapDrawable ?: return + Palette.from(bitmapCover.bitmap).generate { + if (recycler == null) return@generate + val currentNightMode = + recycler.resources!!.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + val colorBack = view.context.getResourceColor( + android.R.attr.colorBackground + ) + val backDropColor = + (if (currentNightMode == Configuration.UI_MODE_NIGHT_NO) it?.getLightVibrantColor( + colorBack + ) + else it?.getDarkVibrantColor(colorBack)) ?: colorBack + coverColor = backDropColor + getHeader()?.setBackDrop(backDropColor) + if (toolbarIsColored) { + val translucentColor = ColorUtils.setAlphaComponent(backDropColor, 175) + (activity as MainActivity).toolbar.setBackgroundColor(translucentColor) + activity?.window?.statusBarColor = translucentColor + } + } + } + + override fun onLoadCleared(placeholder: Drawable?) { } + }) + } + + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + presenter.isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() + presenter.headerItem.isLocked = presenter.isLockedFromSearch + presenter.fetchChapters(refreshTracker == null) + if (refreshTracker != null) { + trackingBottomSheet?.refreshItem(refreshTracker ?: 0) + presenter.refreshTrackers() + refreshTracker = null + } + val isCurrentController = router?.backstack?.lastOrNull()?.controller() == + this + if (isCurrentController) { + setStatusBarAndToolbar() + } + } + + fun showError(message: String) { + swipe_refresh?.isRefreshing = presenter.isLoading + view?.snack(message) + } + + fun updateChapterDownload(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status, presenter.isLockedFromSearch, + download.progress) + } + + private fun isTabletSize(): Boolean { + val activity = activity ?: return false + if ((activity.resources.configuration.screenLayout and Configuration + .SCREENLAYOUT_SIZE_MASK) < Configuration.SCREENLAYOUT_SIZE_LARGE) + return false + val displayMetrics = DisplayMetrics() + activity.windowManager?.defaultDisplay?.getMetrics(displayMetrics) + return displayMetrics.widthPixels.pxToDp >= 720 + } + + fun hasTabletHeight(): Boolean { + val activity = activity ?: return false + if ((activity.resources.configuration.screenLayout and Configuration + .SCREENLAYOUT_SIZE_MASK) < Configuration.SCREENLAYOUT_SIZE_LARGE) return false + val displayMetrics = DisplayMetrics() + activity.windowManager?.defaultDisplay?.getMetrics(displayMetrics) + return displayMetrics.heightPixels.pxToDp >= 720 + } + + private fun getHolder(chapter: Chapter): ChapterHolder? { + return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + } + + private fun getHeader(): MangaHeaderHolder? { + return if (isTablet) tabletRecycler?.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder + else recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type == ControllerChangeType.PUSH_ENTER || type == ControllerChangeType.POP_ENTER) { + setActionBar(true) + setStatusBarAndToolbar() + } else if (type == ControllerChangeType.PUSH_EXIT || type == ControllerChangeType.POP_EXIT) { + if (router.backstack.lastOrNull()?.controller() is DialogController) + return + if (type == ControllerChangeType.POP_EXIT) { + setActionBar(false) + presenter.cancelScope() + } + colorAnimator?.cancel() + + val colorSecondary = activity?.getResourceColor( + R.attr.colorSecondary + ) ?: Color.BLACK + (activity as MainActivity).appbar.setBackgroundColor(colorSecondary) + (activity as MainActivity).toolbar.setBackgroundColor(colorSecondary) + + activity?.window?.statusBarColor = activity?.getResourceColor(android.R.attr + .statusBarColor) ?: colorSecondary + } + } + + private fun setActionBar(forThis: Boolean) { + val currentNightMode = + activity!!.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + // if the theme is using inverted toolbar color + if (currentNightMode == Configuration.UI_MODE_NIGHT_NO && ThemeUtil.isBlueTheme( + presenter.preferences.theme() + ) + ) { + if (forThis) + (activity as MainActivity).appbar.context.setTheme(R.style + .ThemeOverlay_AppCompat_DayNight_ActionBar) + else + (activity as MainActivity).appbar.context.setTheme(R.style + .Theme_ActionBar_Dark_DayNight) + + val iconPrimary = view?.context?.getResourceColor( + if (forThis) android.R.attr.textColorPrimary + else R.attr.actionBarTintColor + ) ?: Color.BLACK + (activity as MainActivity).toolbar.setTitleTextColor(iconPrimary) + (activity as MainActivity).drawerArrow?.color = iconPrimary + (activity as MainActivity).toolbar.overflowIcon?.setTint(iconPrimary) + if (forThis) activity!!.main_content.systemUiVisibility = + activity!!.main_content.systemUiVisibility.or( + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + ) + else activity!!.main_content.systemUiVisibility = + activity!!.main_content.systemUiVisibility.rem( + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + ) + } + } + + fun setRefresh(enabled: Boolean) { + swipe_refresh.isRefreshing = enabled + } + + fun updateHeader() { + swipe_refresh?.isRefreshing = presenter.isLoading + adapter?.setChapters(presenter.chapters) + addMangaHeader() + activity?.invalidateOptionsMenu() + } + + fun updateChapters(chapters: List) { + swipe_refresh?.isRefreshing = presenter.isLoading + if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { + launchUI { swipe_refresh?.isRefreshing = true } + presenter.fetchChaptersFromSource() + } + adapter?.setChapters(chapters) + addMangaHeader() + activity?.invalidateOptionsMenu() + } + + fun refreshAdapter() = adapter?.notifyDataSetChanged() + + override fun onItemClick(view: View?, position: Int): Boolean { + val chapter = (adapter?.getItem(position) as? ChapterItem)?.chapter ?: return false + if (actionMode != null) { + if (startingDLChapterPos == null) { + adapter?.addSelection(position) + (recycler.findViewHolderForAdapterPosition(position) as? BaseFlexibleViewHolder) + ?.toggleActivation() + (recycler.findViewHolderForAdapterPosition(position) as? ChapterHolder) + ?.notifyStatus(Download.CHECKED, false, 0) + startingDLChapterPos = position + actionMode?.invalidate() + } else { + val startingPosition = startingDLChapterPos ?: return false + var chapterList = listOf() + when { + startingPosition > position -> + chapterList = presenter.chapters.subList(position - 1, startingPosition) + startingPosition <= position -> + chapterList = presenter.chapters.subList(startingPosition - 1, position) + } + downloadChapters(chapterList) + adapter?.removeSelection(startingPosition) + (recycler.findViewHolderForAdapterPosition(startingPosition) as? BaseFlexibleViewHolder) + ?.toggleActivation() + startingDLChapterPos = null + destroyActionModeIfNeeded() + } + return false + } + openChapter(chapter) + return false + } + + override fun onItemLongClick(position: Int) { + val adapter = adapter ?: return + val item = (adapter.getItem(position) as? ChapterItem) ?: return + val itemView = getHolder(item)?.itemView ?: return + val popup = PopupMenu(itemView.context, itemView) + chapterPopupMenu = position to popup + + // Inflate our menu resource into the PopupMenu's Menu + popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) + + popup.setOnMenuItemClickListener { menuItem -> + val chapters = listOf(item) + when (menuItem.itemId) { + R.id.action_mark_previous_as_read -> markPreviousAsRead(item) + } + chapterPopupMenu = null + true + } + + // Finally show the PopupMenu + popup.show() + } + + fun dismissPopup(position: Int) { + if (chapterPopupMenu != null && chapterPopupMenu?.first == position) { + chapterPopupMenu?.second?.dismiss() + chapterPopupMenu = null + } + } + + private fun markPreviousAsRead(chapter: ChapterItem) { + val adapter = adapter ?: return + val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items + val chapterPos = chapters.indexOf(chapter) + if (chapterPos != -1) { + markAsRead(chapters.take(chapterPos)) + } + } + + fun bookmarkChapter(position: Int) { + val item = adapter?.getItem(position) as? ChapterItem ?: return + val chapter = item.chapter + val bookmarked = item.bookmark + bookmarkChapters(listOf(item), !bookmarked) + snack?.dismiss() + snack = view?.snack( + if (bookmarked) R.string.removed_bookmark + else R.string.bookmarked, Snackbar.LENGTH_INDEFINITE + ) { + setAction(R.string.undo) { + bookmarkChapters(listOf(item), bookmarked) + } + } + (activity as? MainActivity)?.setUndoSnackBar(snack) + } + + fun toggleReadChapter(position: Int) { + val item = adapter?.getItem(position) as? ChapterItem ?: return + val chapter = item.chapter + val lastRead = chapter.last_page_read + val pagesLeft = chapter.pages_left + val read = item.chapter.read + presenter.markChaptersRead(listOf(item), !read, false) + snack?.dismiss() + snack = view?.snack( + if (read) R.string.marked_as_unread + else R.string.marked_as_read, Snackbar.LENGTH_INDEFINITE + ) { + var undoing = false + setAction(R.string.undo) { + presenter.markChaptersRead(listOf(item), read, true, lastRead, pagesLeft) + undoing = true + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!undoing && !read && presenter.preferences.removeAfterMarkedAsRead()) { + presenter.deleteChapters(listOf(item)) + } + } + }) + } + (activity as? MainActivity)?.setUndoSnackBar(snack) + } + + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + presenter.bookmarkChapters(chapters, bookmarked) + } + + private fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + } + + private fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + } + + private fun openChapter(chapter: Chapter) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, manga!!, chapter) + startActivity(intent) + } + + override fun onDestroyView(view: View) { + snack?.dismiss() + presenter.onDestroy() + adapter = null + trackingBottomSheet = null + super.onDestroyView(view) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.manga_details, menu) + val editItem = menu.findItem(R.id.action_edit) + editItem.isVisible = presenter.manga.favorite && !presenter.isLockedFromSearch + editItem.title = view?.context?.getString(if (manga?.source == LocalSource.ID) + R.string.edit else R.string.edit_cover) + menu.findItem(R.id.action_download).isVisible = !presenter.isLockedFromSearch && + manga?.source != LocalSource.ID + menu.findItem(R.id.action_add_to_home_screen).isVisible = !presenter.isLockedFromSearch + menu.findItem(R.id.action_mark_all_as_read).isVisible = + presenter.getNextUnreadChapter() != null && !presenter.isLockedFromSearch + menu.findItem(R.id.action_mark_all_as_unread).isVisible = + presenter.anyUnread() && !presenter.isLockedFromSearch + menu.findItem(R.id.action_remove_downloads).isVisible = + presenter.hasDownloads() && !presenter.isLockedFromSearch && + manga?.source != LocalSource.ID + menu.findItem(R.id.remove_non_bookmarked).isVisible = + presenter.hasBookmark() && !presenter.isLockedFromSearch + menu.findItem(R.id.action_migrate).isVisible = !presenter.isLockedFromSearch && + manga?.source != LocalSource.ID && presenter.manga.favorite + menu.findItem(R.id.action_migrate).title = view?.context?.getString(R.string.migrate_, + presenter.manga.mangaType(view!!.context)) + val iconPrimary = view?.context?.getResourceColor(android.R.attr.textColorPrimary) + ?: Color.BLACK + menu.findItem(R.id.action_download).icon?.mutate()?.setTint(iconPrimary) + editItem.icon?.mutate()?.setTint(iconPrimary) + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = resources?.getString(R.string.search_chapters) + searchItem.icon?.mutate()?.setTint(iconPrimary) + searchItem.collapseActionView() + if (query.isNotEmpty()) { + searchItem.expandActionView() + searchView.setQuery(query, true) + searchView.clearFocus() + } + + setOnQueryTextChangeListener(searchView) { + query = it ?: "" + if (!isTablet) { + if (query.isNotEmpty()) getHeader()?.collapse() + else getHeader()?.expand() + } + + adapter?.setFilter(query) + adapter?.performFilter() + true + } + searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() }) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_edit -> { + if (manga?.source == LocalSource.ID) { + editMangaDialog = EditMangaDialog( + this, presenter.manga + ) + editMangaDialog?.showDialog(router) + } else { + if (manga?.hasCustomCover() == true) { + MaterialDialog(activity!!).listItems(items = listOf( + view!!.context.getString( + R.string.edit_cover + ), view!!.context.getString( + R.string.reset_cover + ) + ), waitForPositiveButton = false, selection = { _, index, _ -> + when (index) { + 0 -> changeCover() + else -> presenter.clearCover() + } + }).show() + } else { + changeCover() + } + } + } + R.id.action_open_in_web_view -> openInWebView() + R.id.action_add_to_home_screen -> addToHomeScreen() + R.id.action_refresh_tracking -> presenter.refreshTrackers() + R.id.action_migrate -> + PreMigrationController.navigateToMigration( + presenter.preferences.skipPreMigration().getOrDefault(), + router, + listOf(manga!!.id!!)) + R.id.action_mark_all_as_read -> { + MaterialDialog(view!!.context).message(R.string.mark_all_chapters_as_read) + .positiveButton(R.string.mark_as_read) { + markAsRead(presenter.chapters) + }.negativeButton(android.R.string.cancel).show() + } + R.id.remove_all, R.id.remove_read, R.id.remove_non_bookmarked -> massDeleteChapters(item.itemId) + R.id.action_mark_all_as_unread -> markAsUnread(presenter.chapters) + R.id.download_next, R.id.download_next_5, R.id.download_custom, R.id.download_unread, R.id.download_all -> downloadChapters( + item.itemId + ) + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. + */ + override fun prepareToShareManga() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && coverDrawable != null) + GlideApp.with(activity!!).asBitmap().load(presenter.manga).into(object : + CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + presenter.shareManga(resource) + } + override fun onLoadCleared(placeholder: Drawable?) {} + + override fun onLoadFailed(errorDrawable: Drawable?) { + shareManga() + } + }) + else shareManga() + } + + /** + * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. + */ + fun shareManga(cover: File? = null) { + val context = view?.context ?: return + + val source = presenter.source as? HttpSource ?: return + val stream = cover?.getUriCompat(context) + try { + val url = source.mangaDetailsRequest(presenter.manga).url.toString() + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/*" + putExtra(Intent.EXTRA_TEXT, url) + putExtra(Intent.EXTRA_TITLE, presenter.manga.title) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + if (stream != null) { + clipData = ClipData.newRawUri(null, stream) + } + } + startActivity(Intent.createChooser(intent, context.getString(R.string.share))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + override fun openInWebView() { + val source = presenter.source as? HttpSource ?: return + + val url = try { + source.mangaDetailsRequest(presenter.manga).url.toString() + } catch (e: Exception) { + return + } + + val activity = activity ?: return + val intent = WebViewActivity.newIntent(activity.applicationContext, source.id, url, presenter.manga + .title) + startActivity(intent) + } + + private fun massDeleteChapters(choice: Int) { + val chaptersToDelete = when (choice) { + R.id.remove_all -> presenter.chapters + R.id.remove_non_bookmarked -> presenter.chapters.filter { !it.bookmark } + R.id.remove_read -> presenter.chapters.filter { it.read } + else -> emptyList() + }.filter { it.isDownloaded } + if (chaptersToDelete.isNotEmpty()) { + massDeleteChapters(chaptersToDelete) + } + } + + private fun massDeleteChapters(chapters: List) { + val context = view?.context ?: return + MaterialDialog(context).message( + text = context.resources.getQuantityString( + R.plurals.remove_n_chapters, chapters.size, chapters.size + ) + ).positiveButton(R.string.remove) { + presenter.deleteChapters(chapters) + }.negativeButton(android.R.string.cancel).show() + } + + private fun downloadChapters(choice: Int) { + val chaptersToDownload = when (choice) { + R.id.download_next -> presenter.getUnreadChaptersSorted().take(1) + R.id.download_next_5 -> presenter.getUnreadChaptersSorted().take(5) + R.id.download_custom -> { + createActionModeIfNeeded() + return + } + R.id.download_unread -> presenter.chapters.filter { !it.read } + R.id.download_all -> presenter.chapters + else -> emptyList() + } + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + } + + override fun startDownloadNow(position: Int) { + val chapter = (adapter?.getItem(position) as? ChapterItem) ?: return + presenter.startDownloadingNow(chapter) + } + + private fun downloadChapters(chapters: List) { + val view = view ?: return + presenter.downloadChapters(chapters) + val text = view.context.getString(R.string.add_x_to_library, presenter.manga.mangaType + (view.context).toLowerCase(Locale.ROOT)) + if (!presenter.manga.favorite && (snack == null || + snack?.getText() != text)) { + snack = view.snack(text, Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.add) { + presenter.setFavorite(true) + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (snack == transientBottomBar) snack = null + } + }) + } + (activity as? MainActivity)?.setUndoSnackBar(snack) + } + } + + /** + * Add a shortcut of the manga to the home screen + */ + private fun addToHomeScreen() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // TODO are transformations really unsupported or is it just the Pixel Launcher? + createShortcutForShape() + } else { + ChooseShapeDialog(this).showDialog(router) + } + } + + /** + * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when + * the resource is available. + * + * @param i The shape index to apply. Defaults to circle crop transformation. + */ + fun createShortcutForShape(i: Int = 0) { + if (activity == null) return + GlideApp.with(activity!!) + .asBitmap() + .load(presenter.manga) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .apply { + when (i) { + 0 -> circleCrop() + 1 -> transform(RoundedCorners(5)) + 2 -> transform(CropSquareTransformation()) + 3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star)) + } + } + .into(object : CustomTarget(128, 128) { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + createShortcut(resource) + } + + override fun onLoadCleared(placeholder: Drawable?) { } + + override fun onLoadFailed(errorDrawable: Drawable?) { + activity?.toast(R.string.could_not_create_shortcut) + } + }) + } + + /** + * Create shortcut using ShortcutManager. + * + * @param icon The image of the shortcut. + */ + private fun createShortcut(icon: Bitmap) { + val activity = activity ?: return + + // Create the shortcut intent. + val shortcutIntent = activity.intent + .setAction(MainActivity.SHORTCUT_MANGA) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(MANGA_EXTRA, presenter.manga.id) + + // Check if shortcut placement is supported + if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { + val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}" + + // Create shortcut info + val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId) + .setShortLabel(presenter.manga.title) + .setIcon(IconCompat.createWithBitmap(icon)) + .setIntent(shortcutIntent) + .build() + + val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create the CallbackIntent. + val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo) + + // Configure the intent so that the broadcast receiver gets the callback successfully. + PendingIntent.getBroadcast(activity, 0, intent, 0) + } else { + NotificationReceiver.shortcutCreatedBroadcast(activity) + } + + // Request shortcut. + ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo, + successCallback.intentSender) + } + } + + override fun startDownloadRange(position: Int) { + if (actionMode == null) createActionModeIfNeeded() + onItemClick(null, position) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.manga_details_controller, container, false) + } + + override fun coverColor(): Int? = coverColor + override fun topCoverHeight(): Int = headerHeight + + override fun readNextChapter() { + if (activity is SearchActivity && presenter.isLockedFromSearch) { + SecureActivityDelegate.promptLockIfNeeded(activity) + return + } + val item = presenter.getNextUnreadChapter() + if (item != null) { + openChapter(item.chapter) + } else if (snack == null || snack?.getText() != view?.context?.getString( + R.string.next_chapter_not_found)) { + snack = view?.snack(R.string.next_chapter_not_found, Snackbar.LENGTH_LONG) { + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (snack == transientBottomBar) snack = null + } + }) + } + } + } + + override fun downloadChapter(position: Int) { + val view = view ?: return + val chapter = (adapter?.getItem(position) as? ChapterItem) ?: return + if (actionMode != null) { + onItemClick(null, position) + return + } + if (chapter.status != Download.NOT_DOWNLOADED && chapter.status != Download.ERROR) { + presenter.deleteChapters(listOf(chapter)) + } else { + if (chapter.status == Download.ERROR) + DownloadService.start(view.context) + else + downloadChapters(listOf(chapter)) + } + } + + override fun tagClicked(text: String) { + val firstController = router.backstack.first()?.controller() + if (firstController is LibraryController && router.backstack.size == 2) { + router.handleBack() + firstController.search(text) + } + } + + override fun showChapterFilter() { + ChaptersSortBottomSheet(this).show() + } + + private fun isLocked(): Boolean { + if (presenter.isLockedFromSearch) { + SecureActivityDelegate.promptLockIfNeeded(activity) + return true + } + return false + } + + override fun favoriteManga(longPress: Boolean) { + if (isLocked()) return + val manga = presenter.manga + val categories = presenter.getCategories() + if (longPress && categories.isNotEmpty()) { + if (!manga.favorite) { + presenter.toggleFavorite() + showAddedSnack() + } + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected).showDialog( + router + ) + } else { + if (!manga.favorite) { + toggleMangaFavorite() + } else { + val headerHolder = getHeader() ?: return + val popup = PopupMenu(view!!.context, headerHolder.favorite_button) + popup.menu.add(R.string.remove_from_library) + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener { + toggleMangaFavorite() + true + } + popup.show() + } + } + } + + private fun toggleMangaFavorite() { + val manga = presenter.manga + if (presenter.toggleFavorite()) { + val categories = presenter.getCategories() + val defaultCategoryId = presenter.preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } + when { + defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) + defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category + presenter.moveMangaToCategory(manga, null) + else -> { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog( + this, + listOf(manga), + categories, + preselected + ).showDialog(router) + } + } + showAddedSnack() + } else { + showRemovedSnack() + } + } + + private fun showAddedSnack() { + val view = view ?: return + snack?.dismiss() + snack = view.snack(view.context.getString(R.string.added_to_library)) + } + + private fun showRemovedSnack() { + val view = view ?: return + snack?.dismiss() + snack = view.snack( + view.context.getString(R.string.removed_from_library), + Snackbar.LENGTH_INDEFINITE + ) { + setAction(R.string.undo) { + presenter.setFavorite(true) + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!presenter.manga.favorite) presenter.confirmDeletion() + } + }) + } + val favButton = getHeader()?.favorite_button + (activity as? MainActivity)?.setUndoSnackBar(snack, favButton) + } + + override fun mangaPresenter(): MangaDetailsPresenter = presenter + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.moveMangaToCategories(manga, categories) + } + + /** + * Copies a string to clipboard + * + * @param content the actual text to copy to the board + * @param label Label to show to the user describing the content + */ + override fun copyToClipboard(content: String, label: Int) { + if (content.isBlank()) return + + val activity = activity ?: return + val view = view ?: return + + val contentType = view.context.getString(label) + val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText(contentType, content)) + + snack = view.snack(view.context.getString(R.string._copied_to_clipboard, contentType)) + } + + override fun handleBack(): Boolean { + if (manga_cover_full?.visibility == View.VISIBLE) { + manga_cover_full?.performClick() + return true + } + return super.handleBack() + } + + override fun showTrackingSheet() { + if (isLocked()) return + trackingBottomSheet = + TrackingBottomSheet(this) + trackingBottomSheet?.show() + } + + fun refreshTracking(trackings: List) { + trackingBottomSheet?.onNextTrackings(trackings) + } + + fun onTrackSearchResults(results: List) { + trackingBottomSheet?.onSearchResults(results) + } + + fun refreshTracker() { + getHeader()?.updateTracking() + } + + fun trackRefreshDone() { + trackingBottomSheet?.onRefreshDone() + } + + fun trackRefreshError(error: Exception) { + Timber.e(error) + trackingBottomSheet?.onRefreshError(error) + } + + fun trackSearchError(error: Exception) { + Timber.e(error) + trackingBottomSheet?.onSearchResultsError(error) + } + + /** + * Creates the action mode if it's not created already. + */ + private fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + (activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT) + val view = activity?.window?.currentFocus ?: return + val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + ?: return + imm.hideSoftInputFromWindow(view.windowToken, 0) + if (adapter?.mode != SelectableAdapter.Mode.MULTI) { + adapter?.mode = SelectableAdapter.Mode.MULTI + } + } + } + + /** + * Destroys the action mode. + */ + private fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + return true + } + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) { + actionMode = null + setStatusBarAndToolbar() + if (startingDLChapterPos != null) { + val item = adapter?.getItem(startingDLChapterPos!!) as? ChapterItem + (recycler.findViewHolderForAdapterPosition(startingDLChapterPos!!) as? ChapterHolder)?.notifyStatus( + item?.status ?: Download.NOT_DOWNLOADED, false, 0 + ) + } + startingDLChapterPos = null + adapter?.mode = SelectableAdapter.Mode.IDLE + adapter?.clearSelection() + return + } + + /** + * Called to set the last used catalogue at the top of the view. + */ + private fun addMangaHeader() { + if (tabletAdapter?.scrollableHeaders?.isEmpty() == true) { + tabletAdapter?.removeAllScrollableHeaders() + tabletAdapter?.addScrollableHeader(presenter.headerItem) + adapter?.removeAllScrollableHeaders() + adapter?.addScrollableHeader(presenter.tabletChapterHeaderItem!!) + } else if (!isTablet && adapter?.scrollableHeaders?.isEmpty() == true) { + adapter?.removeAllScrollableHeaders() + adapter?.addScrollableHeader(presenter.headerItem) + } + } + + private fun setStatusBarAndToolbar() { + activity?.window?.statusBarColor = if (toolbarIsColored) { + val translucentColor = ColorUtils.setAlphaComponent(coverColor ?: Color.TRANSPARENT, 175) + (activity as MainActivity).toolbar.setBackgroundColor(translucentColor) + translucentColor + } else Color.TRANSPARENT + (activity as MainActivity).appbar.setBackgroundColor(Color.TRANSPARENT) + (activity as MainActivity).toolbar.setBackgroundColor(activity?.window?.statusBarColor + ?: Color.TRANSPARENT) + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + mode?.title = view?.context?.getString(if (startingDLChapterPos == null) + R.string.select_starting_chapter else R.string.select_ending_chapter) + return false + } + + fun changeCover() { + if (manga?.favorite == true) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + startActivityForResult( + Intent.createChooser(intent, + resources?.getString(R.string.select_cover_image)), + 101 + ) + } else { + activity?.toast(R.string.must_be_in_library_to_edit) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == 101) { + if (data == null || resultCode != Activity.RESULT_OK) return + val activity = activity ?: return + try { + val uri = data.data ?: return + if (editMangaDialog != null) editMangaDialog?.updateCover(uri) + else { + presenter.editCoverWithStream(uri) + setPaletteColor() + } + } catch (error: IOException) { + activity.toast(R.string.failed_to_update_cover) + Timber.e(error) + } + } + } + + override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + swipe_refresh.isEnabled = actionState != ItemTouchHelper.ACTION_STATE_SWIPE + } + + override fun onItemMove(fromPosition: Int, toPosition: Int) { + } + + override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { + return true + } + + override fun zoomImageFromThumb(thumbView: View) { + // If there's an animation in progress, cancel it immediately and proceed with this one. + currentAnimator?.cancel() + + // Load the high-resolution "zoomed-in" image. + val expandedImageView = manga_cover_full ?: return + val fullBackdrop = full_backdrop + val image = coverDrawable ?: return + expandedImageView.setImageDrawable(image) + + // Hide the thumbnail and show the zoomed-in view. When the animation + // begins, it will position the zoomed-in view in the place of the + // thumbnail. + thumbView.alpha = 0f + expandedImageView.visibility = View.VISIBLE + fullBackdrop.visibility = View.VISIBLE + + // Set the pivot point to 0 to match thumbnail + + swipe_refresh.isEnabled = false + + val rect = Rect() + thumbView.getGlobalVisibleRect(rect) + expandedImageView.updateLayoutParams { + height = thumbView.height + width = thumbView.width + topMargin = rect.top + leftMargin = rect.left + rightMargin = rect.right + bottomMargin = rect.bottom + } + expandedImageView.requestLayout() + + expandedImageView.post { + val defMargin = 16.dpToPx + expandedImageView.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + width = ViewGroup.LayoutParams.MATCH_PARENT + topMargin = defMargin + headerHeight + leftMargin = defMargin + rightMargin = defMargin + bottomMargin = defMargin + recycler.paddingBottom + } + val shortAnimationDuration = resources?.getInteger( + android.R.integer.config_shortAnimTime + ) ?: 0 + + // TransitionSet for the full cover because using animation for this SUCKS + val transitionSet = TransitionSet() + val bound = ChangeBounds() + transitionSet.addTransition(bound) + val changeImageTransform = ChangeImageTransform() + transitionSet.addTransition(changeImageTransform) + transitionSet.duration = shortAnimationDuration.toLong() + TransitionManager.beginDelayedTransition(frame_layout, transitionSet) + + // AnimationSet for backdrop because idk how to use TransitionSet + currentAnimator = AnimatorSet().apply { + play( + ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f, 0.5f) + ) + duration = shortAnimationDuration.toLong() + interpolator = DecelerateInterpolator() + addListener(object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator) { + TransitionManager.endTransitions(frame_layout) + currentAnimator = null + } + + override fun onAnimationCancel(animation: Animator) { + TransitionManager.endTransitions(frame_layout) + currentAnimator = null + } + }) + start() + } + + expandedImageView.setOnClickListener { + currentAnimator?.cancel() + + val rect2 = Rect() + thumbView.getGlobalVisibleRect(rect2) + expandedImageView.updateLayoutParams { + height = thumbView.height + width = thumbView.width + topMargin = rect2.top + leftMargin = rect2.left + rightMargin = rect2.right + bottomMargin = rect2.bottom + } + + // Zoom out back to tc thumbnail + val transitionSet2 = TransitionSet() + val bound2 = ChangeBounds() + transitionSet2.addTransition(bound2) + val changeImageTransform2 = ChangeImageTransform() + transitionSet2.addTransition(changeImageTransform2) + transitionSet2.duration = shortAnimationDuration.toLong() + TransitionManager.beginDelayedTransition(frame_layout, transitionSet2) + + // Animation to remove backdrop and hide the full cover + currentAnimator = AnimatorSet().apply { + play(ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f)) + duration = shortAnimationDuration.toLong() + interpolator = DecelerateInterpolator() + addListener(object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator) { + thumbView.alpha = 1f + expandedImageView.visibility = View.GONE + fullBackdrop.visibility = View.GONE + swipe_refresh.isEnabled = true + currentAnimator = null + } + + override fun onAnimationCancel(animation: Animator) { + thumbView.alpha = 1f + expandedImageView.visibility = View.GONE + fullBackdrop.visibility = View.GONE + swipe_refresh.isEnabled = true + currentAnimator = null + } + }) + start() + } + } + } + } + + companion object { + const val UPDATE_EXTRA = "update" + const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig" + + const val FROM_CATALOGUE_EXTRA = "from_catalogue" + const val MANGA_EXTRA = "manga" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt new file mode 100644 index 0000000000..54345c0dc8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -0,0 +1,859 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.app.Application +import android.graphics.Bitmap +import android.net.Uri +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.download.model.DownloadQueue +import eu.kanade.tachiyomi.data.library.LibraryServiceListener +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.fetchChapterListAsync +import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.ui.manga.track.TrackItem +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.storage.DiskUtil +import eu.kanade.tachiyomi.util.system.executeOnIO +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream +import java.util.Date + +class MangaDetailsPresenter( + private val controller: MangaDetailsController, + val manga: Manga, + val source: Source, + val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : DownloadQueue.DownloadListener, LibraryServiceListener { + + private var scope = CoroutineScope(Job() + Dispatchers.Default) + + var isLockedFromSearch = false + var hasRequested = false + var isLoading = false + var scrollType = 0 + private val volumeRegex = Regex("""(vol|volume)\.? *([0-9]+)?""", RegexOption.IGNORE_CASE) + private val seasonRegex = Regex("""(Season |S)([0-9]+)?""") + + private val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } + var tracks = emptyList() + + var trackList: List = emptyList() + + var chapters: List = emptyList() + private set + + var headerItem = MangaHeaderItem(manga, controller.fromCatalogue) + var tabletChapterHeaderItem: MangaHeaderItem? = null + + fun onCreate() { + headerItem.startExpanded = controller.hasTabletHeight() || headerItem.startExpanded + headerItem.isTablet = controller.isTablet + if (controller.isTablet) { + tabletChapterHeaderItem = MangaHeaderItem(manga, false) + tabletChapterHeaderItem?.isChapterHeader = true + } + isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() + headerItem.isLocked = isLockedFromSearch + downloadManager.addListener(this) + LibraryUpdateService.setListener(this) + tracks = db.getTracks(manga).executeAsBlocking() + if (!manga.initialized) { + isLoading = true + controller.setRefresh(true) + controller.updateHeader() + refreshAll() + } else { + updateChapters() + controller.updateChapters(this.chapters) + } + fetchTrackings() + refreshTrackers() + } + + fun onDestroy() { + downloadManager.removeListener(this) + LibraryUpdateService.removeListener(this) + } + + fun cancelScope() { + scope.cancel() + } + + fun fetchChapters(andTracking: Boolean = true) { + scope.launch { + getChapters() + if (andTracking) refreshTracking() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + private suspend fun getChapters() { + val chapters = db.getChapters(manga).executeOnIO().map { it.toModel() } + + // Find downloaded chapters + setDownloadedChapters(chapters) + + // Store the last emission + this.chapters = applyChapterFilters(chapters) + } + + private fun updateChapters(fetchedChapters: List? = null) { + val chapters = + (fetchedChapters ?: db.getChapters(manga).executeAsBlocking()).map { it.toModel() } + + // Find downloaded chapters + setDownloadedChapters(chapters) + + // Store the last emission + this.chapters = applyChapterFilters(chapters) + } + + /** + * Finds and assigns the list of downloaded chapters. + * + * @param chapters the list of chapter from the database. + */ + private fun setDownloadedChapters(chapters: List) { + for (chapter in chapters) { + if (downloadManager.isChapterDownloaded(chapter, manga)) { + chapter.status = Download.DOWNLOADED + } else if (downloadManager.hasQueue()) { + chapter.status = downloadManager.queue.find { it.chapter.id == chapter.id } + ?.status ?: 0 + } + } + } + + override fun updateDownload(download: Download) { + chapters.find { it.id == download.chapter.id }?.download = download + scope.launch(Dispatchers.Main) { + controller.updateChapterDownload(download) + } + } + + /** + * Converts a chapter from the database to an extended model, allowing to store new fields. + */ + private fun Chapter.toModel(): ChapterItem { + // Create the model object. + val model = ChapterItem(this, manga) + model.isLocked = isLockedFromSearch + + // Find an active download for this chapter. + val download = downloadManager.queue.find { it.chapter.id == id } + + if (download != null) { + // If there's an active download, assign it. + model.download = download + } + return model + } + + /** + * Sets the active display mode. + * @param hide set title to hidden + */ + fun hideTitle(hide: Boolean) { + manga.displayMode = if (hide) Manga.DISPLAY_NUMBER else Manga.DISPLAY_NAME + db.updateFlags(manga).executeAsBlocking() + controller.refreshAdapter() + } + + /** + * Sets the sorting method and requests an UI update. + * @param sort the sorting mode. + */ + fun setSorting(sort: Int) { + manga.sorting = sort + db.updateFlags(manga).executeAsBlocking() + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyDownloaded() = manga.downloadedFilter == Manga.SHOW_DOWNLOADED + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyBookmarked() = manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED + + /** + * Whether the display only unread filter is enabled. + */ + fun onlyUnread() = manga.readFilter == Manga.SHOW_UNREAD + + /** + * Whether the display only read filter is enabled. + */ + fun onlyRead() = manga.readFilter == Manga.SHOW_READ + + /** + * Whether the sorting method is descending or ascending. + */ + fun sortDescending() = manga.sortDescending(globalSort()) + + /** + * Applies the view filters to the list of chapters obtained from the database. + * @param chapterList the list of chapters from the database + * @return an observable of the list of chapters filtered and sorted. + */ + private fun applyChapterFilters(chapterList: List): List { + if (isLockedFromSearch) + return chapterList + var chapters = chapterList + if (onlyUnread()) { + chapters = chapters.filter { !it.read } + } else if (onlyRead()) { + chapters = chapters.filter { it.read } + } + if (onlyDownloaded()) { + chapters = chapters.filter { it.isDownloaded || it.manga.source == LocalSource.ID } + } + if (onlyBookmarked()) { + chapters = chapters.filter { it.bookmark } + } + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { + Manga.SORTING_SOURCE -> when (sortDescending()) { + true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } + false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } + } + Manga.SORTING_NUMBER -> when (sortDescending()) { + true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } + false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } + } + else -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } + } + chapters = chapters.sortedWith(Comparator(sortFunction)) + getScrollType(chapters) + return chapters + } + + private fun getScrollType(chapters: List) { + scrollType = when { + hasMultipleVolumes(chapters) -> MULTIPLE_VOLUMES + hasMultipleSeasons(chapters) -> MULTIPLE_SEASONS + hasHundredsOfChapters(chapters) -> HUNDREDS_OF_CHAPTERS + hasTensOfChapters(chapters) -> TENS_OF_CHAPTERS + else -> 0 + } + } + + fun getGroupNumber(chapter: ChapterItem): Int? { + val groups = volumeRegex.find(chapter.name)?.groups + if (groups != null) return groups[2]?.value?.toIntOrNull() + val seasonGroups = seasonRegex.find(chapter.name)?.groups + if (seasonGroups != null) return seasonGroups[2]?.value?.toIntOrNull() + return null + } + + private fun getVolumeNumber(chapter: ChapterItem): Int? { + val groups = volumeRegex.find(chapter.name)?.groups + if (groups != null) return groups[2]?.value?.toIntOrNull() + return null + } + + private fun getSeasonNumber(chapter: ChapterItem): Int? { + val groups = seasonRegex.find(chapter.name)?.groups + if (groups != null) return groups[2]?.value?.toIntOrNull() + return null + } + + private fun hasMultipleVolumes(chapters: List): Boolean { + val volumeSet = mutableSetOf() + chapters.forEach { + val volNum = getVolumeNumber(it) + if (volNum != null) { + volumeSet.add(volNum) + if (volumeSet.size >= 3) return true + } + } + return false + } + + private fun hasMultipleSeasons(chapters: List): Boolean { + val volumeSet = mutableSetOf() + chapters.forEach { + val volNum = getSeasonNumber(it) + if (volNum != null) { + volumeSet.add(volNum) + if (volumeSet.size >= 3) return true + } + } + return false + } + + private fun hasHundredsOfChapters(chapters: List): Boolean { + return chapters.size > 300 + } + + private fun hasTensOfChapters(chapters: List): Boolean { + return chapters.size in 21..300 + } + /** + * Returns the next unread chapter or null if everything is read. + */ + fun getNextUnreadChapter(): ChapterItem? { + return chapters.sortedByDescending { it.source_order }.find { !it.read } + } + + fun anyUnread(): Boolean = chapters.any { !it.read } + fun hasBookmark(): Boolean = chapters.any { it.bookmark } + fun hasDownloads(): Boolean = chapters.any { it.isDownloaded } + + fun getUnreadChaptersSorted() = + chapters.filter { !it.read && it.status == Download.NOT_DOWNLOADED }.distinctBy { it.name } + .sortedByDescending { it.source_order } + + /** + * Returns the next unread chapter or null if everything is read. + */ + fun getNewestChapterTime(): Long? { + return chapters.maxBy { it.date_upload }?.date_upload + } + + fun getLatestChapter(): Float? { + return chapters.maxBy { it.chapter_number }?.chapter_number + } + + fun startDownloadingNow(chapter: Chapter) { + downloadManager.startDownloadNow(chapter) + } + + /** + * Downloads the given list of chapters with the manager. + * @param chapters the list of chapters to download. + */ + fun downloadChapters(chapters: List) { + downloadManager.downloadChapters(manga, chapters.filter { !it.isDownloaded }) + } + + fun restartDownloads() { + if (downloadManager.isPaused()) downloadManager.startDownloads() + } + + /** + * Deletes the given list of chapter. + * @param chapters the list of chapters to delete. + */ + fun deleteChapters(chapters: List, update: Boolean = true) { + downloadManager.deleteChapters(chapters, manga, source) + + chapters.forEach { chapter -> + this.chapters.find { it.id == chapter.id }?.apply { + status = Download.NOT_DOWNLOADED + download = null + } + } + + if (update) controller.updateChapters(this.chapters) + } + + fun refreshAll() { + scope.launch { + isLoading = true + var mangaError: java.lang.Exception? = null + var chapterError: java.lang.Exception? = null + val chapters = async(Dispatchers.IO) { + try { + source.fetchChapterListAsync(manga) + } catch (e: Exception) { + chapterError = e + emptyList() + } ?: emptyList() + } + val thumbnailUrl = manga.thumbnail_url + val nManga = async(Dispatchers.IO) { + try { + source.fetchMangaDetailsAsync(manga) + } catch (e: java.lang.Exception) { + mangaError = e + null + } + } + + val networkManga = nManga.await() + if (networkManga != null) { + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { + MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + withContext(Dispatchers.Main) { controller.setPaletteColor() } + } + } + val finChapters = chapters.await() + if (finChapters.isNotEmpty()) { + syncChaptersWithSource(db, finChapters, manga, source) + withContext(Dispatchers.IO) { updateChapters() } + } + isLoading = false + if (chapterError == null) withContext(Dispatchers.Main) { controller.updateChapters(this@MangaDetailsPresenter.chapters) } + else { + withContext(Dispatchers.Main) { + controller.showError( + trimException(mangaError!!) + ) + } + return@launch + } + if (mangaError != null) withContext(Dispatchers.Main) { + controller.showError( + trimException(mangaError!!) + ) + } + } + } + + /** + * Requests an updated list of chapters from the source. + */ + fun fetchChaptersFromSource() { + hasRequested = true + isLoading = true + + scope.launch(Dispatchers.IO) { + val chapters = try { + source.fetchChapterListAsync(manga) + } catch (e: Exception) { + withContext(Dispatchers.Main) { controller.showError(trimException(e)) } + return@launch + } ?: listOf() + isLoading = false + try { + syncChaptersWithSource(db, chapters, manga, source) + + updateChapters() + withContext(Dispatchers.Main) { controller.updateChapters(this@MangaDetailsPresenter.chapters) } + } catch (e: java.lang.Exception) { + controller.showError(trimException(e)) + } + } + } + + private fun trimException(e: java.lang.Exception): String { + return (if (e.message?.contains(": ") == true) e.message?.split(": ")?.drop(1) + ?.joinToString(": ") + else e.message) ?: preferences.context.getString(R.string.unknown_error) + } + + /** + * Bookmarks the given list of chapters. + * @param selectedChapters the list of chapters to bookmark. + */ + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + scope.launch(Dispatchers.IO) { + selectedChapters.forEach { + it.bookmark = bookmarked + } + db.updateChaptersProgress(selectedChapters).executeAsBlocking() + getChapters() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + /** + * Mark the selected chapter list as read/unread. + * @param selectedChapters the list of selected chapters. + * @param read whether to mark chapters as read or unread. + */ + fun markChaptersRead( + selectedChapters: List, + read: Boolean, + deleteNow: Boolean = true, + lastRead: Int? = null, + pagesLeft: Int? = null + ) { + scope.launch(Dispatchers.IO) { + selectedChapters.forEach { + it.read = read + if (!read) { + it.last_page_read = lastRead ?: 0 + it.pages_left = pagesLeft ?: 0 + } + } + db.updateChaptersProgress(selectedChapters).executeAsBlocking() + if (read && deleteNow && preferences.removeAfterMarkedAsRead()) { + deleteChapters(selectedChapters, false) + } + getChapters() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + /** + * Sets the sorting order and requests an UI update. + */ + fun setSortOrder(descend: Boolean) { + manga.setChapterOrder(if (descend) Manga.SORT_DESC else Manga.SORT_ASC) + asyncUpdateMangaAndChapters() + } + + fun globalSort() = preferences.chaptersDescAsDefault().getOrDefault() + + fun setGlobalChapterSort(descend: Boolean) { + preferences.chaptersDescAsDefault().set(descend) + manga.setSortToGlobal() + asyncUpdateMangaAndChapters() + } + + /** + * Sets the sorting method and requests an UI update. + */ + fun setSortMethod(bySource: Boolean) { + manga.sorting = if (bySource) Manga.SORTING_SOURCE else Manga.SORTING_NUMBER + asyncUpdateMangaAndChapters() + } + + /** + * Removes all filters and requests an UI update. + */ + fun setFilters(read: Boolean, unread: Boolean, downloaded: Boolean, bookmarked: Boolean) { + manga.readFilter = when { + read -> Manga.SHOW_READ + unread -> Manga.SHOW_UNREAD + else -> Manga.SHOW_ALL + } + manga.downloadedFilter = if (downloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL + manga.bookmarkedFilter = if (bookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL + asyncUpdateMangaAndChapters() + } + + private fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { + scope.launch { + if (!justChapters) db.updateFlags(manga).executeOnIO() + updateChapters() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + fun currentFilters(): String { + val filtersId = mutableListOf() + filtersId.add(if (onlyRead()) R.string.read else null) + filtersId.add(if (onlyUnread()) R.string.unread else null) + filtersId.add(if (onlyDownloaded()) R.string.downloaded else null) + filtersId.add(if (onlyBookmarked()) R.string.bookmarked else null) + return filtersId.filterNotNull().joinToString(", ") { preferences.context.getString(it) } + } + + fun toggleFavorite(): Boolean { + manga.favorite = !manga.favorite + + when (manga.favorite) { + true -> manga.date_added = Date().time + false -> manga.date_added = 0 + } + + db.insertManga(manga).executeAsBlocking() + controller.updateHeader() + return manga.favorite + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + fun getCategories(): List { + return db.getCategories().executeAsBlocking() + } + + /** + * Move the given manga to the category. + * + * @param manga the manga to move. + * @param category the selected category, or null for default category. + */ + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) + } + + /** + * Move the given manga to categories. + * + * @param manga the manga to move. + * @param categories the selected categories. + */ + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) + } + + /** + * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. + * + * @param manga the manga to get categories from. + * @return Array of category ids the manga is in, if none returns default id + */ + fun getMangaCategoryIds(manga: Manga): Array { + val categories = db.getCategoriesForManga(manga).executeAsBlocking() + return categories.mapNotNull { it.id }.toTypedArray() + } + + fun confirmDeletion() { + coverCache.deleteFromCache(manga.thumbnail_url) + db.resetMangaInfo(manga).executeAsBlocking() + downloadManager.deleteManga(manga, source) + asyncUpdateMangaAndChapters(true) + } + + fun setFavorite(favorite: Boolean) { + if (manga.favorite == favorite) { + return + } + toggleFavorite() + } + + override fun onUpdateManga(manga: LibraryManga) { + if (manga.id == this.manga.id) { + fetchChapters() + } + } + + fun shareManga(cover: Bitmap) { + val context = Injekt.get() + + val destDir = File(context.cacheDir, "shared_image") + + scope.launch(Dispatchers.IO) { + destDir.deleteRecursively() + try { + val image = saveImage(cover, destDir, manga) + if (image != null) controller.shareManga(image) + else controller.shareManga() + } catch (e: java.lang.Exception) { + } + } + } + + private fun saveImage(cover: Bitmap, directory: File, manga: Manga): File? { + directory.mkdirs() + + // Build destination file. + val filename = DiskUtil.buildValidFilename("${manga.title} - Cover.jpg") + + val destFile = File(directory, filename) + val stream: OutputStream = FileOutputStream(destFile) + cover.compress(Bitmap.CompressFormat.JPEG, 75, stream) + stream.flush() + stream.close() + return destFile + } + + fun updateManga( + title: String?, + author: String?, + artist: String?, + uri: Uri?, + description: String?, + tags: Array? + ) { + if (manga.source == LocalSource.ID) { + manga.title = if (title.isNullOrBlank()) manga.url else title.trim() + manga.author = author?.trim() + manga.artist = artist?.trim() + manga.description = description?.trim() + val tagsString = tags?.joinToString(", ") { it.capitalize() } + manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim() + LocalSource(downloadManager.context).updateMangaInfo(manga) + db.updateMangaInfo(manga).executeAsBlocking() + } + if (uri != null) editCoverWithStream(uri) + controller.updateHeader() + } + + fun clearCover() { + if (manga.hasCustomCover()) { + coverCache.deleteFromCache(manga.thumbnail_url!!) + manga.thumbnail_url = manga.thumbnail_url?.removePrefix("Custom-") + db.insertManga(manga).executeAsBlocking() + MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + controller.updateHeader() + controller.setPaletteColor() + } + } + + fun editCoverWithStream(uri: Uri): Boolean { + val inputStream = + downloadManager.context.contentResolver.openInputStream(uri) ?: return false + if (manga.source == LocalSource.ID) { + LocalSource.updateCover(downloadManager.context, manga, inputStream) + return true + } + + if (manga.favorite) { + if (!manga.hasCustomCover()) { + manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}" + db.insertManga(manga).executeAsBlocking() + } + coverCache.copyToCache(manga.thumbnail_url!!, inputStream) + MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + return true + } + return false + } + + fun isTracked(): Boolean = + loggedServices.any { service -> tracks.any { it.sync_id == service.id } } + + fun hasTrackers(): Boolean = loggedServices.isNotEmpty() + + // Tracking + private fun fetchTrackings() { + scope.launch { + trackList = loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + } + } + + private suspend fun refreshTracking() { + tracks = withContext(Dispatchers.IO) { db.getTracks(manga).executeAsBlocking() } + trackList = loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + withContext(Dispatchers.Main) { controller.refreshTracking(trackList) } + } + + fun refreshTrackers() { + scope.launch { + trackList.filter { it.track != null }.map { item -> + withContext(Dispatchers.IO) { + val trackItem = try { + item.service.refresh(item.track!!) + } catch (e: Exception) { + trackError(e) + null + } + if (trackItem != null) { + db.insertTrack(trackItem).executeAsBlocking() + trackItem + } else item.track + } + } + refreshTracking() + } + } + + fun trackSearch(query: String, service: TrackService) { + scope.launch(Dispatchers.IO) { + val results = try { + service.search(query) + } catch (e: Exception) { + withContext(Dispatchers.Main) { controller.trackSearchError(e) } + null + } + if (!results.isNullOrEmpty()) { + withContext(Dispatchers.Main) { controller.onTrackSearchResults(results) } + } + } + } + + fun registerTracking(item: Track?, service: TrackService) { + if (item != null) { + item.manga_id = manga.id!! + + scope.launch { + val binding = try { + service.bind(item) + } catch (e: Exception) { + trackError(e) + null + } + withContext(Dispatchers.IO) { + if (binding != null) db.insertTrack(binding).executeAsBlocking() + } + refreshTracking() + } + } else { + scope.launch { + withContext(Dispatchers.IO) { + db.deleteTrackForManga(manga, service).executeAsBlocking() + } + refreshTracking() + } + } + } + + private fun updateRemote(track: Track, service: TrackService) { + scope.launch { + val binding = try { + service.update(track) + } catch (e: Exception) { + trackError(e) + null + } + if (binding != null) { + withContext(Dispatchers.IO) { db.insertTrack(binding).executeAsBlocking() } + refreshTracking() + } else trackRefreshDone() + } + } + + private fun trackRefreshDone() { + scope.launch(Dispatchers.Main) { controller.trackRefreshDone() } + } + + private fun trackError(error: Exception) { + scope.launch(Dispatchers.Main) { controller.trackRefreshError(error) } + } + + fun setStatus(item: TrackItem, index: Int) { + val track = item.track!! + track.status = item.service.getStatusList()[index] + if (item.service.isCompletedStatus(index) && track.total_chapters > 0) + track.last_chapter_read = track.total_chapters + updateRemote(track, item.service) + } + + fun setScore(item: TrackItem, index: Int) { + val track = item.track!! + track.score = item.service.indexToScore(index) + updateRemote(track, item.service) + } + + fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { + val track = item.track!! + track.last_chapter_read = chapterNumber + updateRemote(track, item.service) + } + + companion object { + const val MULTIPLE_VOLUMES = 1 + const val TENS_OF_CHAPTERS = 2 + const val HUNDREDS_OF_CHAPTERS = 3 + const val MULTIPLE_SEASONS = 4 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt new file mode 100644 index 0000000000..e91e9c20eb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -0,0 +1,307 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color +import android.view.MotionEvent +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.signature.ObjectKey +import com.google.android.material.button.MaterialButton +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +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.resetStrokeColor +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.visible +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.manga_details_controller.* +import kotlinx.android.synthetic.main.manga_header_item.* + +class MangaHeaderHolder( + private val view: View, + private val adapter: MangaDetailsAdapter, + startExpanded: Boolean, + isTablet: Boolean = false +) : BaseFlexibleViewHolder(view, adapter) { + + init { + chapter_layout.setOnClickListener { adapter.delegate.showChapterFilter() } + if (start_reading_button != null) { + start_reading_button.setOnClickListener { adapter.delegate.readNextChapter() } + top_view.updateLayoutParams { + height = adapter.delegate.topCoverHeight() + } + more_button.setOnClickListener { expandDesc() } + manga_summary.setOnClickListener { expandDesc() } + manga_summary.setOnLongClickListener { + if (manga_summary.isTextSelectable && !adapter.recyclerView.canScrollVertically(-1)) { + (adapter.delegate as MangaDetailsController).swipe_refresh.isEnabled = false + } + false + } + manga_summary.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_UP) (adapter.delegate as MangaDetailsController).swipe_refresh.isEnabled = + true + false + } + less_button.setOnClickListener { collapseDesc() } + manga_genres_tags.setOnTagClickListener { + adapter.delegate.tagClicked(it) + } + webview_button.setOnClickListener { adapter.delegate.openInWebView() } + share_button.setOnClickListener { adapter.delegate.prepareToShareManga() } + favorite_button.setOnClickListener { + adapter.delegate.favoriteManga(false) + } + favorite_button.setOnLongClickListener { + adapter.delegate.favoriteManga(true) + true + } + manga_full_title.setOnLongClickListener { + adapter.delegate.copyToClipboard(manga_full_title.text.toString(), R.string.title) + true + } + manga_author.setOnLongClickListener { + adapter.delegate.copyToClipboard(manga_author.text.toString(), R.string.author) + true + } + manga_cover.setOnClickListener { adapter.delegate.zoomImageFromThumb(cover_card) } + track_button.setOnClickListener { adapter.delegate.showTrackingSheet() } + if (startExpanded) expandDesc() + else collapseDesc() + if (isTablet) chapter_layout.gone() + } + } + + private fun expandDesc() { + if (more_button.visibility == View.VISIBLE) { + manga_summary.maxLines = Integer.MAX_VALUE + manga_summary.setTextIsSelectable(true) + manga_genres_tags.visible() + less_button.visible() + more_button_group.gone() + } + } + + private fun collapseDesc() { + manga_summary.setTextIsSelectable(false) + manga_summary.maxLines = 3 + manga_summary.setOnClickListener { expandDesc() } + manga_genres_tags.gone() + less_button.gone() + more_button_group.visible() + } + + fun bindChapters() { + val presenter = adapter.delegate.mangaPresenter() + val count = presenter.chapters.size + chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count) + filters_text.text = presenter.currentFilters() + } + + @SuppressLint("SetTextI18n") + fun bind(item: MangaHeaderItem, manga: Manga) { + val presenter = adapter.delegate.mangaPresenter() + manga_full_title.text = manga.title + + if (manga.genre.isNullOrBlank().not()) manga_genres_tags.setTags( + manga.genre?.split(", ")?.map(String::trim) + ) + else manga_genres_tags.setTags(emptyList()) + + if (manga.author == manga.artist || manga.artist.isNullOrBlank()) manga_author.text = + manga.author?.trim() + else { + manga_author.text = "${manga.author?.trim()}, ${manga.artist}" + } + manga_summary.text = + if (manga.description.isNullOrBlank()) itemView.context.getString(R.string.no_description) + else manga.description?.trim() + + if (item.isLocked) sub_item_group.referencedIds = + intArrayOf(R.id.manga_summary, R.id.manga_summary_label, R.id.button_layout) + else sub_item_group.referencedIds = intArrayOf( + R.id.start_reading_button, + R.id.manga_summary, + R.id.manga_summary_label, + R.id.button_layout + ) + + manga_summary.post { + if (sub_item_group.visibility != View.GONE) { + if ((manga_summary.lineCount < 3 && manga.genre.isNullOrBlank()) || less_button.visibility == View.VISIBLE) { + more_button_group.gone() + } else more_button_group.visible() + } + if (adapter.hasFilter()) collapse() + else expand() + } + manga_summary_label.text = itemView.context.getString( + R.string.about_this_, manga.mangaType(itemView.context) + ) + with(favorite_button) { + icon = ContextCompat.getDrawable( + itemView.context, when { + item.isLocked -> R.drawable.ic_lock_white_24dp + manga.favorite -> R.drawable.ic_heart_24dp + else -> R.drawable.ic_heart_outline_24dp + } + ) + text = itemView.resources.getString( + when { + item.isLocked -> R.string.unlock + manga.favorite -> R.string.in_library + else -> R.string.add_to_library + } + ) + checked(!item.isLocked && manga.favorite) + } + true_backdrop.setBackgroundColor( + adapter.delegate.coverColor() + ?: itemView.context.getResourceColor(android.R.attr.colorBackground) + ) + + val tracked = presenter.isTracked() && !item.isLocked + + with(track_button) { + visibleIf(presenter.hasTrackers()) + text = itemView.context.getString( + if (tracked) R.string.tracked + else R.string.tracking + ) + + icon = ContextCompat.getDrawable( + itemView.context, + if (tracked) R.drawable.ic_check_white_24dp else R.drawable.ic_sync_black_24dp + ) + checked(tracked) + } + + with(start_reading_button) { + val nextChapter = presenter.getNextUnreadChapter() + visibleIf(presenter.chapters.isNotEmpty() && !item.isLocked) + isEnabled = (nextChapter != null) + text = if (nextChapter != null) { + val number = adapter.decimalFormat.format(nextChapter.chapter_number.toDouble()) + if (nextChapter.chapter_number > 0) resources.getString( + if (nextChapter.last_page_read > 0) R.string.continue_reading_chapter_ + else R.string.start_reading_chapter_, number + ) + else { + resources.getString( + if (nextChapter.last_page_read > 0) R.string.continue_reading + else R.string.start_reading + ) + } + } else { + resources.getString(R.string.all_chapters_read) + } + } + + val count = presenter.chapters.size + chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count) + + top_view.updateLayoutParams { + height = adapter.delegate.topCoverHeight() + } + + manga_status.text = (itemView.context.getString( + when (manga.status) { + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed + else -> R.string.unknown_status + } + )) + manga_source.text = presenter.source.toString() + + filters_text.text = presenter.currentFilters() + + if (manga.source == LocalSource.ID) { + webview_button.gone() + share_button.gone() + } + + if (!manga.initialized) return + GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) + .into(manga_cover) + GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())).centerCrop() + .transition(DrawableTransitionOptions.withCrossFade()).into(backdrop) + } + + private fun MaterialButton.checked(checked: Boolean) { + if (checked) { + backgroundTintList = ColorStateList.valueOf( + ColorUtils.setAlphaComponent( + context.getResourceColor(R.attr.colorAccent), 75 + ) + ) + strokeColor = ColorStateList.valueOf(Color.TRANSPARENT) + } else { + resetStrokeColor() + backgroundTintList = + ContextCompat.getColorStateList(context, android.R.color.transparent) + } + } + + fun setTopHeight(newHeight: Int) { + top_view.updateLayoutParams { + height = newHeight + } + } + + fun setBackDrop(color: Int) { + true_backdrop.setBackgroundColor(color) + } + + fun updateTracking() { + val presenter = adapter.delegate.mangaPresenter() + val tracked = presenter.isTracked() + with(track_button) { + text = itemView.context.getString(if (tracked) R.string.tracked + else R.string.tracking) + + icon = ContextCompat.getDrawable(itemView.context, if (tracked) R.drawable + .ic_check_white_24dp else R.drawable.ic_sync_black_24dp) + checked(tracked) + } + } + + fun collapse() { + sub_item_group.gone() + if (more_button.visibility == View.VISIBLE || more_button.visibility == View.INVISIBLE) + more_button_group.invisible() + else { + less_button.gone() + manga_genres_tags.gone() + } + } + + fun expand() { + sub_item_group.visible() + if (more_button.visibility == View.VISIBLE || more_button.visibility == View.INVISIBLE) more_button_group.visible() + else { + less_button.visible() + manga_genres_tags.visible() + } + } + + override fun onLongClick(view: View?): Boolean { + super.onLongClick(view) + return false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt new file mode 100644 index 0000000000..c569ec6b34 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt @@ -0,0 +1,51 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga + +class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) : + AbstractFlexibleItem() { + + var isTablet = false + var isChapterHeader = false + var isLocked = false + + override fun getLayoutRes(): Int { + return if (isChapterHeader) R.layout.chapter_header_item else R.layout.manga_header_item + } + + override fun isSelectable(): Boolean { + return false + } + + override fun isSwipeable(): Boolean { + return false + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MangaHeaderHolder { + return MangaHeaderHolder(view, adapter as MangaDetailsAdapter, startExpanded, isTablet) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: MangaHeaderHolder, + position: Int, + payloads: MutableList? + ) { + if (isChapterHeader) holder.bindChapters() + else holder.bind(this, manga) + } + + override fun equals(other: Any?): Boolean { + return (this === other) + } + + override fun hashCode(): Int { + return manga.id!!.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt new file mode 100644 index 0000000000..7de4583a37 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible + +open class BaseChapterAdapter>( + obj: DownloadInterface +) : FlexibleAdapter(null, obj, true) { + + val baseDelegate = obj + + interface DownloadInterface { + fun downloadChapter(position: Int) + fun startDownloadNow(position: Int) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt new file mode 100644 index 0000000000..48da5324ad --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.View +import androidx.appcompat.widget.PopupMenu +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlinx.android.synthetic.main.download_button.* + +open class BaseChapterHolder( + view: View, + private val adapter: BaseChapterAdapter<*> +) : BaseFlexibleViewHolder(view, adapter) { + + init { + download_button?.setOnClickListener { downloadOrRemoveMenu() } + } + + private fun downloadOrRemoveMenu() { + val chapter = adapter.getItem(adapterPosition) as? BaseChapterItem<*, *> ?: return + if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) { + adapter.baseDelegate.downloadChapter(adapterPosition) + } else { + download_button.post { + // Create a PopupMenu, giving it the clicked view for an anchor + val popup = PopupMenu(download_button.context, download_button) + + // Inflate our menu resource into the PopupMenu's Menu + popup.menuInflater.inflate(R.menu.chapter_download, popup.menu) + + popup.menu.findItem(R.id.action_start).isVisible = chapter.status == Download.QUEUE + + // Hide download and show delete if the chapter is downloaded + if (chapter.status != Download.DOWNLOADED) popup.menu.findItem(R.id.action_delete).title = download_button.context.getString( + R.string.cancel + ) + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_delete -> adapter.baseDelegate.downloadChapter(adapterPosition) + R.id.action_start -> adapter.baseDelegate.startDownloadNow(adapterPosition) + } + true + } + + // Finally show the PopupMenu + popup.show() + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterItem.kt new file mode 100644 index 0000000000..dece86fe00 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterItem.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.AbstractSectionableItem +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.source.model.Page + +abstract class BaseChapterItem>( + val chapter: +Chapter, + header: H? = null +) : + AbstractSectionableItem(header), + Chapter by chapter { + + private var _status: Int = 0 + + val progress: Int + get() { + val pages = download?.pages ?: return 0 + return pages.map(Page::progress).average().toInt() + } + + var status: Int + get() = download?.status ?: _status + set(value) { _status = value } + + @Transient var download: Download? = null + + val isDownloaded: Boolean + get() = status == Download.DOWNLOADED + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is BaseChapterItem<*, *>) { + return chapter.id == other.chapter.id + } + return false + } + + override fun hashCode(): Int { + return (chapter.id ?: 0L).hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt index fa5945db49..e983ac855e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt @@ -1,122 +1,128 @@ package eu.kanade.tachiyomi.ui.manga.chapter +import android.text.format.DateUtils import android.view.View -import android.widget.PopupMenu +import androidx.core.content.ContextCompat import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.manga.MangaDetailsAdapter import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.setVectorCompat +import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.chapters_item.* -import java.util.* +import kotlinx.android.synthetic.main.download_button.* +import java.util.Date class ChapterHolder( - private val view: View, - private val adapter: ChaptersAdapter -) : BaseFlexibleViewHolder(view, adapter) { + view: View, + private val adapter: MangaDetailsAdapter +) : BaseChapterHolder(view, adapter) { + private var localSource = false init { - // We need to post a Runnable to show the popup to make sure that the PopupMenu is - // correctly positioned. The reason being that the view may change position before the - // PopupMenu is shown. - chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } + download_button.setOnLongClickListener { + adapter.delegate.startDownloadRange(adapterPosition) + true + } } fun bind(item: ChapterItem, manga: Manga) { val chapter = item.chapter - + val isLocked = item.isLocked chapter_title.text = when (manga.displayMode) { Manga.DISPLAY_NUMBER -> { val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - itemView.context.getString(R.string.display_mode_chapter, number) + itemView.context.getString(R.string.chapter_, number) } else -> chapter.name } - // Set the correct drawable for dropdown and update the tint to match theme. - chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color)) + localSource = manga.source == LocalSource.ID + download_button.visibleIf(!localSource) + + if (isLocked) download_button.gone() // Set correct text color - chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) - if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) + chapter_title.setTextColor( + if (chapter.read && !isLocked) adapter.readColor else adapter.unreadColor + ) + if (chapter.bookmark && !isLocked) chapter_title.setTextColor(adapter.bookmarkedColor) + + val statuses = mutableListOf() if (chapter.date_upload > 0) { - chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload)) - chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) - } else { - chapter_date.text = "" + statuses.add( + DateUtils.getRelativeTimeSpanString( + chapter.date_upload, Date().time, DateUtils.HOUR_IN_MILLIS + ).toString() + ) } - //add scanlator if exists - chapter_scanlator.text = chapter.scanlator - //allow longer titles if there is no scanlator (most sources) - if (chapter_scanlator.text.isNullOrBlank()) { - chapter_title.maxLines = 2 - chapter_scanlator.gone() - } else { - chapter_title.maxLines = 1 + if (!chapter.read && chapter.last_page_read > 0 && chapter.pages_left > 0 && !isLocked) { + statuses.add( + itemView.resources.getQuantityString( + R.plurals.pages_left, chapter.pages_left, chapter.pages_left + ) + ) + } else if (!chapter.read && chapter.last_page_read > 0 && !isLocked) { + statuses.add( + itemView.context.getString( + R.string.page_, chapter.last_page_read + 1 + ) + ) } - chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) { - itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1) - } else { - "" + if (!chapter.scanlator.isNullOrBlank()) { + statuses.add(chapter.scanlator!!) } - notifyStatus(item.status) - } - - fun notifyStatus(status: Int) = with(download_text) { - when (status) { - Download.QUEUE -> setText(R.string.chapter_queued) - Download.DOWNLOADING -> setText(R.string.chapter_downloading) - Download.DOWNLOADED -> setText(R.string.chapter_downloaded) - Download.ERROR -> setText(R.string.chapter_error) - else -> text = "" + if (front_view.translationX == 0f) { + read.setImageDrawable( + ContextCompat.getDrawable( + read.context, if (item.read) R.drawable.ic_eye_off_24dp + else R.drawable.ic_eye_24dp + ) + ) + bookmark.setImageDrawable( + ContextCompat.getDrawable( + read.context, if (item.bookmark) R.drawable.ic_bookmark_off_24dp + else R.drawable.ic_bookmark_24dp + ) + ) } + chapter_scanlator.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) + chapter_scanlator.text = statuses.joinToString(" • ") + notifyStatus( + if (adapter.isSelected(adapterPosition)) Download.CHECKED else item.status, + item.isLocked, + item.progress + ) + resetFrontView() } - private fun showPopupMenu(view: View) { - val item = adapter.getItem(adapterPosition) ?: return - - // Create a PopupMenu, giving it the clicked view for an anchor - val popup = PopupMenu(view.context, view) - - // Inflate our menu resource into the PopupMenu's Menu - popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) - - val chapter = item.chapter - - // Hide download and show delete if the chapter is downloaded - if (item.isDownloaded) { - popup.menu.findItem(R.id.action_download).isVisible = false - popup.menu.findItem(R.id.action_delete).isVisible = true - } + override fun getFrontView(): View { + return front_view + } - // Hide bookmark if bookmark - popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark - popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark + override fun getRearRightView(): View { + return right_view + } - // Hide mark as unread when the chapter is unread - if (!chapter.read && chapter.last_page_read == 0) { - popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false - } + override fun getRearLeftView(): View { + return left_view + } - // Hide mark as read when the chapter is read - if (chapter.read) { - popup.menu.findItem(R.id.action_mark_as_read).isVisible = false - } + private fun resetFrontView() { + if (front_view.translationX != 0f) itemView.post { adapter.notifyItemChanged(adapterPosition) } + } - // Set a listener so we are notified if a menu item is clicked - popup.setOnMenuItemClickListener { menuItem -> - adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem) - true + fun notifyStatus(status: Int, locked: Boolean, progress: Int) = with(download_button) { + if (locked) { + gone() + return } - - // Finally show the PopupMenu - popup.show() + download_button.visibleIf(!localSource) + setDownloadStatus(status, progress) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt index 141bbdc783..a9667588cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt @@ -3,53 +3,50 @@ package eu.kanade.tachiyomi.ui.manga.chapter import android.view.View import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.manga.MangaDetailsAdapter -class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), - Chapter by chapter { +class ChapterItem(chapter: Chapter, val manga: Manga) : + BaseChapterItem>(chapter) { - private var _status: Int = 0 - - var status: Int - get() = download?.status ?: _status - set(value) { _status = value } - - @Transient var download: Download? = null - - val isDownloaded: Boolean - get() = status == Download.DOWNLOADED + var isLocked = false override fun getLayoutRes(): Int { return R.layout.chapters_item } - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ChapterHolder { - return ChapterHolder(view, adapter as ChaptersAdapter) + override fun isSelectable(): Boolean { + return true } - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: ChapterHolder, - position: Int, - payloads: MutableList?) { - - holder.bind(this, manga) + override fun isSwipeable(): Boolean { + return !isLocked } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is ChapterItem) { - return chapter.id!! == other.chapter.id!! - } - return false + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ChapterHolder { + return ChapterHolder(view, adapter as MangaDetailsAdapter) } - override fun hashCode(): Int { - return chapter.id!!.hashCode() + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: ChapterHolder, + position: Int, + payloads: MutableList? + ) { + holder.bind(this, manga) } + override fun unbindViewHolder( + adapter: FlexibleAdapter>?, + holder: ChapterHolder?, + position: Int + ) { + super.unbindViewHolder(adapter, holder, position) + (adapter as MangaDetailsAdapter).controller.dismissPopup(position) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt deleted file mode 100644 index 2138a2ba78..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.content.Context -import android.view.MenuItem -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.getResourceColor -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import uy.kohesive.injekt.injectLazy - -class ChaptersAdapter( - controller: ChaptersController, - context: Context -) : FlexibleAdapter(null, controller, true) { - - val preferences: PreferencesHelper by injectLazy() - - var items: List = emptyList() - - val menuItemListener: OnMenuItemClickListener = controller - - val readColor = context.getResourceColor(android.R.attr.textColorHint) - - val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary) - - val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) - - val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() - .apply { decimalSeparator = '.' }) - - val dateFormat: DateFormat = preferences.dateFormat().getOrDefault() - - override fun updateDataSet(items: List?) { - this.items = items ?: emptyList() - super.updateDataSet(items) - } - - fun indexOf(item: ChapterItem): Int { - return items.indexOf(item) - } - - interface OnMenuItemClickListener { - fun onMenuItemClick(position: Int, item: MenuItem) - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt deleted file mode 100644 index c9ab74e405..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ /dev/null @@ -1,578 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.view.clicks -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.getCoordinates -import eu.kanade.tachiyomi.util.view.getText -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import kotlinx.android.synthetic.main.chapters_controller.* -import timber.log.Timber - -class ChaptersController() : NucleusController(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - ChaptersAdapter.OnMenuItemClickListener, - DownloadCustomChaptersDialog.Listener, - DeleteChaptersDialog.Listener { - - constructor(startY: Float?) : this() { - this.startingChapterYPos = startY - } - - /** - * Adapter containing a list of chapters. - */ - private var adapter: ChaptersAdapter? = null - - private var scrollToUnread = true - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - private var snack:Snackbar? = null - /** - * Selected items. Used to restore selections after a rotation. - */ - private val selectedItems = mutableSetOf() - - private var lastClickPosition = -1 - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - var startingChapterYPos:Float? = null - - override fun createPresenter(): ChaptersPresenter { - val ctrl = parentController as MangaController - return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.chapters_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Init RecyclerView and adapter - adapter = ChaptersAdapter(this, view.context) - - recycler.adapter = adapter - recycler.layoutManager = LinearLayoutManager(view.context) - recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) - adapter?.fastScroller = fast_scroller - - val fabBaseMarginBottom = fab?.marginBottom ?: 0 - recycler.doOnApplyWindowInsets { v, insets, padding -> - - fab?.updateLayoutParams { - bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom - } - fast_scroller?.updateLayoutParams { - bottomMargin = insets.systemWindowInsetBottom - } - // offset the recycler by the fab's inset + some inset on top - val scale: Float = v.context.resources.displayMetrics.density - val pixels = (88 * scale + 0.5f).toInt() - v.updatePaddingRelative(bottom = padding.bottom + insets.systemWindowInsetBottom + pixels) - } - swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } - - fab.clicks().subscribeUntilDestroy { - val item = presenter.getNextUnreadChapter() - if (item != null) { - // Create animation listener - val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { - openChapter(item.chapter, true) - } - } - - // Get coordinates and start animation - val coordinates = fab.getCoordinates() - if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else if (snack == null || snack?.getText() != view.context.getString(R.string.no_next_chapter)) { - snack = view.snack(R.string.no_next_chapter, Snackbar.LENGTH_LONG) { - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (snack == transientBottomBar) snack = null - } - }) - } - } - } - } - - override fun onDestroyView(view: View) { - adapter = null - actionMode = null - super.onDestroyView(view) - } - - override fun onActivityResumed(activity: Activity) { - if (view == null) return - - // Check if animation view is visible - if (reveal_view.visibility == View.VISIBLE) { - // Show the unReveal effect - val coordinates = fab.getCoordinates() - reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) - } - super.onActivityResumed(activity) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.chapters, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - // Initialize menu items. - val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return - val menuFilterUnread = menu.findItem(R.id.action_filter_unread) - val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) - val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - if (presenter.onlyRead()) - //Disable unread filter option if read filter is enabled. - menuFilterUnread.isEnabled = false - if (presenter.onlyUnread()) - //Disable read filter option if unread filter is enabled. - menuFilterRead.isEnabled = false - - // Display mode submenu - if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { - menu.findItem(R.id.display_title).isChecked = true - } else { - menu.findItem(R.id.display_chapter_number).isChecked = true - } - - // Sorting mode submenu - if (presenter.manga.sorting == Manga.SORTING_SOURCE) { - menu.findItem(R.id.sort_by_source).isChecked = true - } else { - menu.findItem(R.id.sort_by_number).isChecked = true - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.display_title -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NAME) - } - R.id.display_chapter_number -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NUMBER) - } - - R.id.sort_by_source -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_SOURCE) - } - R.id.sort_by_number -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_NUMBER) - } - - R.id.download_next, R.id.download_next_5, R.id.download_next_10, - R.id.download_custom, R.id.download_unread, R.id.download_all - -> downloadChapters(item.itemId) - - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - R.id.action_sort -> presenter.revertSortOrder() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - fun onNextChapters(chapters: List) { - // If the list is empty, fetch chapters from source if the conditions are met - // We use presenter chapters instead because they are always unfiltered - if (presenter.chapters.isEmpty()) { - initialFetchChapters() - } - - val adapter = adapter ?: return - adapter.updateDataSet(chapters) - - if (selectedItems.isNotEmpty()) { - adapter.clearSelection() // we need to start from a clean state, index may have changed - createActionModeIfNeeded() - selectedItems.forEach { item -> - val position = adapter.indexOf(item) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - } - } - actionMode?.invalidate() - } - scrollToUnread() - } - - private fun scrollToUnread() { - if (adapter?.items.isNullOrEmpty()) return - if (scrollToUnread) { - val index = presenter.getFirstUnreadIndex() ?: return - val centerOfScreen = - if (startingChapterYPos != null) startingChapterYPos!!.toInt() - recycler.top - 96 - else recycler.height / 2 - 96 - (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - index, centerOfScreen - ) - } - scrollToUnread = false - } - - private fun initialFetchChapters() { - // Only fetch if this view is from the catalog and it hasn't requested previously - if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) { - fetchChaptersFromSource() - } - } - - private fun fetchChaptersFromSource() { - swipe_refresh?.isRefreshing = true - presenter.fetchChaptersFromSource() - } - - fun onFetchChaptersDone() { - swipe_refresh?.isRefreshing = false - } - - fun onFetchChaptersError(error: Throwable) { - swipe_refresh?.isRefreshing = false - activity?.toast(error.message) - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder - } - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - override fun onItemClick(view: View?, position: Int): Boolean { - val adapter = adapter ?: return false - val item = adapter.getItem(position) ?: return false - if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - lastClickPosition = position - toggleSelection(position) - return true - } else { - openChapter(item.chapter) - return false - } - } - - override fun onItemLongClick(position: Int) { - createActionModeIfNeeded() - when { - lastClickPosition == -1 -> setSelection(position) - lastClickPosition > position -> for (i in position until lastClickPosition) - setSelection(i) - lastClickPosition < position -> for (i in lastClickPosition + 1..position) - setSelection(i) - else -> setSelection(position) - } - lastClickPosition = position - adapter?.notifyDataSetChanged() - } - - // SELECTIONS & ACTION MODE - - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - adapter.toggleSelection(position) - adapter.notifyDataSetChanged() - if (adapter.isSelected(position)) { - selectedItems.add(item) - } else { - selectedItems.remove(item) - } - actionMode?.invalidate() - } - - private fun setSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - if (!adapter.isSelected(position)) { - adapter.toggleSelection(position) - selectedItems.add(item) - actionMode?.invalidate() - } - } - - private fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } - } - - private fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) - } - } - - private fun destroyActionModeIfNeeded() { - lastClickPosition = -1 - actionMode?.finish() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - @SuppressLint("StringFormatInvalid") - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> showDeleteChaptersConfirmationDialog() - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - adapter?.mode = SelectableAdapter.Mode.SINGLE - adapter?.clearSelection() - selectedItems.clear() - actionMode = null - } - - override fun onDetach(view: View) { - destroyActionModeIfNeeded() - super.onDetach(view) - } - - override fun onMenuItemClick(position: Int, item: MenuItem) { - val chapter = adapter?.getItem(position) ?: return - val chapters = listOf(chapter) - - when (item.itemId) { - R.id.action_download -> downloadChapters(chapters) - R.id.action_bookmark -> bookmarkChapters(chapters, true) - R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) - R.id.action_delete -> deleteChapters(chapters) - R.id.action_mark_as_read -> markAsRead(chapters) - R.id.action_mark_as_unread -> markAsUnread(chapters) - R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter) - } - } - - // SELECTION MODE ACTIONS - - private fun selectAll() { - val adapter = adapter ?: return - adapter.selectAll() - selectedItems.addAll(adapter.items) - actionMode?.invalidate() - } - - private fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - private fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - } - - private fun downloadChapters(chapters: List) { - val view = view - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - if (view != null && !presenter.manga.favorite && (snack == null || - snack?.getText() != view.context.getString(R.string.snack_add_to_library))) { - snack = view.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (snack == transientBottomBar) snack = null - } - }) - } - (activity as? MainActivity)?.setUndoSnackBar(snack) - } - } - - private fun showDeleteChaptersConfirmationDialog() { - DeleteChaptersDialog(this).showDialog(router) - } - - override fun deleteChapters() { - deleteChapters(getSelectedChapters()) - } - - private fun markPreviousAsRead(chapter: ChapterItem) { - val adapter = adapter ?: return - val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = chapters.indexOf(chapter) - if (chapterPos != -1) { - markAsRead(chapters.take(chapterPos)) - } - } - - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - destroyActionModeIfNeeded() - presenter.bookmarkChapters(chapters, bookmarked) - } - - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - if (chapters.isEmpty()) return - presenter.deleteChapters(chapters) - } - - fun onChaptersDeleted(chapters: List) { - //this is needed so the downloaded text gets removed from the item - chapters.forEach { - adapter?.updateItem(it) - } - adapter?.notifyDataSetChanged() - } - - fun onChaptersDeletedError(error: Throwable) { - Timber.e(error) - } - - // OVERFLOW MENU DIALOGS - - private fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - adapter?.notifyDataSetChanged() - } - - private fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - private fun downloadChapters(choice: Int) { - val chaptersToDownload = when (choice) { - R.id.download_next -> getUnreadChaptersSorted().take(1) - R.id.download_next_5 -> getUnreadChaptersSorted().take(5) - R.id.download_next_10 -> getUnreadChaptersSorted().take(10) - R.id.download_custom -> { - showCustomDownloadDialog() - return - } - R.id.download_unread -> presenter.chapters.filter { !it.read } - R.id.download_all -> presenter.chapters - else -> emptyList() - } - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } - - private fun showCustomDownloadDialog() { - DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router) - } - - override fun downloadCustomChapters(amount: Int) { - val chaptersToDownload = getUnreadChaptersSorted().take(amount) - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt deleted file mode 100644 index dfc4867972..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ /dev/null @@ -1,426 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -/** - * Presenter of [ChaptersController]. - */ -class ChaptersPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - val preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get() -) : BasePresenter() { - - /** - * List of chapters of the manga. It's always unfiltered and unsorted. - */ - var chapters: List = emptyList() - private set - - /** - * Subject of list of chapters to allow updating the view without going to DB. - */ - val chaptersRelay: PublishRelay> - by lazy { PublishRelay.create>() } - - /** - * Whether the chapter list has been requested to the source. - */ - var hasRequested = false - private set - - /** - * Subscription to retrieve the new list of chapters from the source. - */ - private var fetchChaptersSubscription: Subscription? = null - - /** - * Subscription to observe download status changes. - */ - private var observeDownloadsSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Prepare the relay. - chaptersRelay.flatMap { applyChapterFilters(it) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(ChaptersController::onNextChapters, - { _, error -> Timber.e(error) }) - - // Add the subscription that retrieves the chapters from the database, keeps subscribed to - // changes, and sends the list of chapters to the relay. - add(db.getChapters(manga).asRxObservable() - .map { chapters -> - // Convert every chapter to a model. - chapters.map { it.toModel() } - } - .doOnNext { chapters -> - // Find downloaded chapters - setDownloadedChapters(chapters) - - // Store the last emission - this.chapters = chapters - - // Listen for download status changes - observeDownloads() - - // Emit the number of chapters to the info tab. - chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number - ?: 0f) - - // Emit the upload date of the most recent chapter - lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload - ?: 0)) - - } - .subscribe { chaptersRelay.call(it) }) - } - - private fun observeDownloads() { - observeDownloadsSubscription?.let { remove(it) } - observeDownloadsSubscription = downloadManager.queue.getStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .filter { download -> download.manga.id == manga.id } - .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(ChaptersController::onChapterStatusChange) { - _, error -> Timber.e(error) - } - } - - /** - * Converts a chapter from the database to an extended model, allowing to store new fields. - */ - private fun Chapter.toModel(): ChapterItem { - // Create the model object. - val model = ChapterItem(this, manga) - - // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == id } - - if (download != null) { - // If there's an active download, assign it. - model.download = download - } - return model - } - - /** - * Finds and assigns the list of downloaded chapters. - * - * @param chapters the list of chapter from the database. - */ - private fun setDownloadedChapters(chapters: List) { - for (chapter in chapters) { - if (downloadManager.isChapterDownloaded(chapter, manga)) { - chapter.status = Download.DOWNLOADED - } - } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - - if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onFetchChaptersDone() - }, ChaptersController::onFetchChaptersError) - } - - /** - * Updates the UI after applying the filters. - */ - private fun refreshChapters() { - chaptersRelay.call(chapters) - } - - /** - * Applies the view filters to the list of chapters obtained from the database. - * @param chapters the list of chapters from the database - * @return an observable of the list of chapters filtered and sorted. - */ - private fun applyChapterFilters(chapters: List): Observable> { - var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) - if (onlyUnread()) { - observable = observable.filter { !it.read } - } else if (onlyRead()) { - observable = observable.filter { it.read } - } - if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID } - } - if (onlyBookmarked()) { - observable = observable.filter { it.bookmark } - } - val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { - Manga.SORTING_SOURCE -> when (sortDescending()) { - true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } - false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } - } - Manga.SORTING_NUMBER -> when (sortDescending()) { - true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } - false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } - } - else -> throw NotImplementedError("Unimplemented sorting method") - } - return observable.toSortedList(sortFunction) - } - - /** - * Called when a download for the active manga changes status. - * @param download the download whose status changed. - */ - fun onDownloadStatusChange(download: Download) { - // Assign the download to the model object. - if (download.status == Download.QUEUE) { - chapters.find { it.id == download.chapter.id }?.let { - if (it.download == null) { - it.download = download - } - } - } - - // Force UI update if downloaded filter active and download finished. - if (onlyDownloaded() && download.status == Download.DOWNLOADED) - refreshChapters() - } - - /** - * Returns the next unread chapter or null if everything is read. - */ - fun getNextUnreadChapter(): ChapterItem? { - return chapters.sortedByDescending { it.source_order }.find { !it.read } - } - - /** - * Mark the selected chapter list as read/unread. - * @param selectedChapters the list of selected chapters. - * @param read whether to mark chapters as read or unread. - */ - fun markChaptersRead(selectedChapters: List, read: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.read = read - if (!read) { - chapter.last_page_read = 0 - } - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Downloads the given list of chapters with the manager. - * @param chapters the list of chapters to download. - */ - fun downloadChapters(chapters: List) { - downloadManager.downloadChapters(manga, chapters) - } - - /** - * Bookmarks the given list of chapters. - * @param selectedChapters the list of chapters to bookmark. - */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.bookmark = bookmarked - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Deletes the given list of chapter. - * @param chapters the list of chapters to delete. - */ - fun deleteChapters(chapters: List) { - Observable.just(chapters) - .doOnNext { deleteChaptersInternal(chapters) } - .doOnNext { if (onlyDownloaded()) refreshChapters() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onChaptersDeleted(chapters) - }, ChaptersController::onChaptersDeletedError) - } - - /** - * Deletes a list of chapters from disk. This method is called in a background thread. - * @param chapters the chapters to delete. - */ - private fun deleteChaptersInternal(chapters: List) { - downloadManager.deleteChapters(chapters, manga, source) - chapters.forEach { - it.status = Download.NOT_DOWNLOADED - it.download = null - } - } - - /** - * Reverses the sorting and requests an UI update. - */ - fun revertSortOrder() { - manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyUnread whether to display only unread chapters or all chapters. - */ - fun setUnreadFilter(onlyUnread: Boolean) { - manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyRead whether to display only read chapters or all chapters. - */ - fun setReadFilter(onlyRead: Boolean) { - manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the download filter and requests an UI update. - * @param onlyDownloaded whether to display only downloaded chapters or all chapters. - */ - fun setDownloadedFilter(onlyDownloaded: Boolean) { - manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the bookmark filter and requests an UI update. - * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. - */ - fun setBookmarkedFilter(onlyBookmarked: Boolean) { - manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - mangaFavoriteRelay.call(true) - } - - /** - * Sets the active display mode. - * @param mode the mode to set. - */ - fun setDisplayMode(mode: Int) { - manga.displayMode = mode - db.updateFlags(manga).executeAsBlocking() - } - - /** - * Sets the sorting method and requests an UI update. - * @param sort the sorting mode. - */ - fun setSorting(sort: Int) { - manga.sorting = sort - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyDownloaded(): Boolean { - return manga.downloadedFilter == Manga.SHOW_DOWNLOADED - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyBookmarked(): Boolean { - return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED - } - - /** - * Whether the display only unread filter is enabled. - */ - fun onlyUnread(): Boolean { - return manga.readFilter == Manga.SHOW_UNREAD - } - - /** - * Whether the display only read filter is enabled. - */ - fun onlyRead(): Boolean { - return manga.readFilter == Manga.SHOW_READ - } - - /** - * Whether the sorting method is descending or ascending. - */ - fun sortDescending(): Boolean { - return manga.sortDescending() - } - - fun getFirstUnreadIndex(): Int? { - if (!manga.favorite) { - return null - } - val index = chapters.sortedByDescending { it.source_order }.indexOfFirst { !it.read } - return if (sortDescending()) (chapters.size - 1) - index - else index - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSortBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSortBottomSheet.kt new file mode 100644 index 0000000000..ab8602eb8a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSortBottomSheet.kt @@ -0,0 +1,144 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.invisible +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import eu.kanade.tachiyomi.util.view.visInvisIf +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.chapter_sort_bottom_sheet.* + +class ChaptersSortBottomSheet(controller: MangaDetailsController) : BottomSheetDialog + (controller.activity!!, R.style.BottomSheetDialogTheme) { + + val activity = controller.activity!! + + private var sheetBehavior: BottomSheetBehavior<*> + + private val presenter = controller.presenter + + init { + // Use activity theme for this layout + val view = activity.layoutInflater.inflate(R.layout.chapter_sort_bottom_sheet, null) + setContentView(view) + + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) + setEdgeToEdge(activity, view) + val height = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + sheetBehavior.peekHeight = 415.dpToPx + height + + sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { } + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.skipCollapsed = true + } + } + }) + } + + override fun onStart() { + super.onStart() + sheetBehavior.skipCollapsed = true + } + + /** + * Called when the sheet is created. It initializes the listeners and values of the preferences. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initGeneralPreferences() + setBottomEdge(hide_titles, activity) + close_button.setOnClickListener { dismiss() } + settings_scroll_view.viewTreeObserver.addOnGlobalLayoutListener { + val isScrollable = + settings_scroll_view!!.height < bottom_sheet.height + + settings_scroll_view.paddingTop + settings_scroll_view.paddingBottom + close_button.visibleIf(isScrollable) + } + + setOnDismissListener { + presenter.setFilters( + show_read.isChecked, + show_unread.isChecked, + show_download.isChecked, + show_bookmark.isChecked + ) + } + } + + private fun initGeneralPreferences() { + show_read.isChecked = presenter.onlyRead() + show_unread.isChecked = presenter.onlyUnread() + show_download.isChecked = presenter.onlyDownloaded() + show_bookmark.isChecked = presenter.onlyBookmarked() + + show_all.isChecked = !(show_read.isChecked || show_unread.isChecked || + show_download.isChecked || show_bookmark.isChecked) + + var defPref = presenter.globalSort() + sort_group.check(if (presenter.manga.sortDescending(defPref)) R.id.sort_newest else + R.id.sort_oldest) + + hide_titles.isChecked = presenter.manga.displayMode != Manga.DISPLAY_NAME + sort_method_group.check(if (presenter.manga.sorting == Manga.SORTING_SOURCE) R.id.sort_by_source else + R.id.sort_by_number) + + set_as_default_sort.visInvisIf(defPref != presenter.manga.sortDescending() && + presenter.manga.usesLocalSort()) + sort_group.setOnCheckedChangeListener { _, checkedId -> + presenter.setSortOrder(checkedId == R.id.sort_newest) + set_as_default_sort.visInvisIf(defPref != presenter.manga.sortDescending() && + presenter.manga.usesLocalSort()) + } + + set_as_default_sort.setOnClickListener { + val desc = sort_group.checkedRadioButtonId == R.id.sort_newest + presenter.setGlobalChapterSort(desc) + defPref = desc + set_as_default_sort.invisible() + } + + sort_method_group.setOnCheckedChangeListener { _, checkedId -> + presenter.setSortMethod(checkedId == R.id.sort_by_source) + } + + hide_titles.setOnCheckedChangeListener { _, isChecked -> + presenter.hideTitle(isChecked) + } + + show_all.setOnCheckedChangeListener(::checkedFilter) + show_read.setOnCheckedChangeListener(::checkedFilter) + show_unread.setOnCheckedChangeListener(::checkedFilter) + show_download.setOnCheckedChangeListener(::checkedFilter) + show_bookmark.setOnCheckedChangeListener(::checkedFilter) + } + + private fun checkedFilter(checkBox: CompoundButton, isChecked: Boolean) { + if (isChecked) { + if (show_all == checkBox) { + show_read.isChecked = false + show_unread.isChecked = false + show_download.isChecked = false + show_bookmark.isChecked = false + } else { + show_all.isChecked = false + if (show_read == checkBox) show_unread.isChecked = false + else if (show_unread == checkBox) show_read.isChecked = false + } + } else if (!show_read.isChecked && !show_unread.isChecked && + !show_download.isChecked && !show_bookmark.isChecked) { + show_all.isChecked = true + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt deleted file mode 100644 index 234fd88c6d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.app.Dialog -import android.os.Bundle -import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class DeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : DeleteChaptersDialog.Listener { - - constructor(target: T) : this() { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialDialog(activity!!).show { - message(R.string.confirm_delete_chapters) - positiveButton(android.R.string.yes) { - (targetController as? Listener)?.deleteChapters() - } - negativeButton(android.R.string.no) - } - } - - interface Listener { - fun deleteChapters() - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt deleted file mode 100644 index 13e2e80f7d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt +++ /dev/null @@ -1,76 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.app.Dialog -import android.os.Bundle -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.customview.customView -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.DialogCustomDownloadView - -/** - * Dialog used to let user select amount of chapters to download. - */ -class DownloadCustomChaptersDialog : DialogController - where T : Controller, T : DownloadCustomChaptersDialog.Listener { - - /** - * Maximum number of chapters to download in download chooser. - */ - private val maxChapters: Int - - /** - * Initialize dialog. - * @param maxChapters maximal number of chapters that user can download. - */ - constructor(target: T, maxChapters: Int) : super(Bundle().apply { - // Add maximum number of chapters to download value to bundle. - putInt(KEY_ITEM_MAX, maxChapters) - }) { - targetController = target - this.maxChapters = maxChapters - } - - /** - * Restore dialog. - * @param bundle bundle containing data from state restore. - */ - @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - // Get maximum chapters to download from bundle - val maxChapters = bundle.getInt(KEY_ITEM_MAX, 0) - this.maxChapters = maxChapters - } - - /** - * Called when dialog is being created. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val activity = activity!! - - // Initialize view that lets user select number of chapters to download. - val view = DialogCustomDownloadView(activity).apply { - setMinMax(0, maxChapters) - } - - // Build dialog. - // when positive dialog is pressed call custom listener. - return MaterialDialog(activity) - .title(R.string.custom_download) - .customView(view = view, scrollable = true) - .positiveButton(android.R.string.ok) { - (targetController as? Listener)?.downloadCustomChapters(view.amount) - } - .negativeButton(android.R.string.cancel) - } - - interface Listener { - fun downloadCustomChapters(amount: Int) - } - - private companion object { - // Key to retrieve max chapters from bundle on process death. - const val KEY_ITEM_MAX = "DownloadCustomChaptersDialog.int.maxChapters" - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/EditMangaDialog.kt deleted file mode 100644 index c965241bd4..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/EditMangaDialog.kt +++ /dev/null @@ -1,181 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.app.Activity -import android.app.Dialog -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.view.WindowManager -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.customview.customView -import com.afollestad.materialdialogs.internal.main.DialogLayout -import com.bluelinelabs.conductor.Router -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.signature.ObjectKey -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.util.lang.chop -import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.edit_manga_dialog.view.* -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException - -class EditMangaDialog : DialogController { - - private var dialogView: View? = null - - private val manga: Manga - - private var customCoverUri:Uri? = null - - private val infoController - get() = targetController as MangaInfoController - - constructor(target: MangaInfoController, manga: Manga) : super(Bundle() - .apply { - putLong(KEY_MANGA, manga.id!!) - }) { - targetController = target - this.manga = manga - } - - @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - manga = Injekt.get().getManga(bundle.getLong(KEY_MANGA)) - .executeAsBlocking()!! - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val dialog = MaterialDialog(activity!!).apply { - customView(viewRes = R.layout.edit_manga_dialog, scrollable = true) - negativeButton(android.R.string.cancel) - positiveButton(R.string.action_save) { onPositiveButtonClick() } - } - dialogView = dialog.view - onViewCreated(dialog.view, savedViewState) - dialog.setOnShowListener { - val dView = (it as? MaterialDialog)?.view - dView?.contentLayout?.scrollView?.scrollTo(0, 0) - } - return dialog - } - - fun onViewCreated(view: View, savedState: Bundle?) { - GlideApp.with(view.context) - .asDrawable() - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .dontAnimate() - .into(view.manga_cover) - val isLocal = manga.source == LocalSource.ID - - if (isLocal) { - if (manga.title != manga.url) - view.manga_title.append(manga.title) - view.manga_title.hint = "${resources?.getString(R.string.title)}: ${manga.url}" - view.manga_author.append(manga.author ?: "") - view.manga_artist.append(manga.artist ?: "") - view.manga_description.append(manga.description ?: "") - view.manga_genres_tags.setTags(manga.genre?.split(", ") ?: emptyList()) - } - else { - if (manga.currentTitle() != manga.originalTitle()) - view.manga_title.append(manga.currentTitle()) - view.manga_title.hint = "${resources?.getString(R.string.title)}: ${manga - .originalTitle()}" - - if (manga.currentAuthor() != manga.originalAuthor()) - view.manga_author.append(manga.currentAuthor()) - if (!manga.originalAuthor().isNullOrBlank()) - view.manga_author.hint = "${resources?.getString(R.string - .manga_info_author_label)}: ${manga.originalAuthor()}" - - if (manga.currentArtist() != manga.originalArtist()) - view.manga_artist.append(manga.currentArtist()) - if (!manga.originalArtist().isNullOrBlank()) - view.manga_artist.hint = "${resources?.getString(R.string - .manga_info_artist_label)}: ${manga.originalArtist()}" - - if (manga.currentDesc() != manga.originalDesc()) - view.manga_description.append(manga.currentDesc()) - if (!manga.originalDesc().isNullOrBlank()) - view.manga_description.hint = "${resources?.getString(R.string.description)}: ${manga - .originalDesc()?.chop(15)}" - if (manga.currentGenres().isNullOrBlank().not()) { - view.manga_genres_tags.setTags(manga.currentGenres()?.split(", ")) - } - } - view.manga_genres_tags.clearFocus() - view.cover_layout.setOnClickListener { - changeCover() - } - view.reset_tags.setOnClickListener { resetTags() } - } - - private fun resetTags() { - if (manga.originalGenres().isNullOrBlank() || manga.source == LocalSource.ID) - dialogView?.manga_genres_tags?.setTags(emptyList()) - else - dialogView?.manga_genres_tags?.setTags(manga.originalGenres()?.split(", ")) - } - - private fun changeCover() { - if (manga.favorite) { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "image/*" - startActivityForResult( - Intent.createChooser(intent, - resources?.getString(R.string.file_select_cover)), - 101 - ) - } else { - activity?.toast(R.string.notification_first_add_to_library) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 101) { - if (data == null || resultCode != Activity.RESULT_OK) return - val activity = activity ?: return - - try { - // Get the file's input stream from the incoming Intent - GlideApp.with(dialogView!!.context) - .load(data.data ?: Uri.EMPTY) - .into(dialogView!!.manga_cover) - customCoverUri = data.data - } catch (error: IOException) { - activity.toast(R.string.notification_cover_update_failed) - Timber.e(error) - } - } - } - - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - dialogView = null - } - - private fun onPositiveButtonClick() { - infoController.presenter.updateManga(dialogView?.manga_title?.text.toString(), - dialogView?.manga_author?.text.toString(), dialogView?.manga_artist?.text.toString(), - customCoverUri, dialogView?.manga_description?.text.toString(), - dialogView?.manga_genres_tags?.tags) - infoController.updateTitle() - } - - private companion object { - const val KEY_MANGA = "manga_id" - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt deleted file mode 100644 index 9c77615287..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ /dev/null @@ -1,871 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.app.Dialog -import android.app.PendingIntent -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.view.animation.DecelerateInterpolator -import android.widget.ImageView -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import androidx.transition.ChangeBounds -import androidx.transition.ChangeImageTransform -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.list.listItemsSingleChoice -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition -import com.bumptech.glide.signature.ObjectKey -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.view.clicks -import com.jakewharton.rxbinding.view.longClicks -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.data.notification.NotificationReceiver -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.storage.getUriCompat -import eu.kanade.tachiyomi.util.view.marginBottom -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.updateLayoutParams -import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import jp.wasabeef.glide.transformations.CropSquareTransformation -import jp.wasabeef.glide.transformations.MaskTransformation -import kotlinx.android.synthetic.main.manga_info_controller.* -import kotlinx.android.synthetic.main.manga_info_controller.manga_cover -import kotlinx.android.synthetic.main.manga_info_controller.manga_genres_tags -import uy.kohesive.injekt.injectLazy -import java.io.File -import java.text.DateFormat -import java.text.DecimalFormat -import java.util.Date -import kotlin.math.max - -/** - * Fragment that shows manga information. - * Uses R.layout.manga_info_controller. - * UI related actions should be called from here. - */ -class MangaInfoController : NucleusController(), - ChangeMangaCategoriesDialog.Listener { - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * Snackbar containing an error message when a request fails. - */ - private var snack: Snackbar? = null - - private var container:View? = null - - // Hold a reference to the current animator, - // so that it can be canceled mid-way. - private var currentAnimator: Animator? = null - - // The system "short" animation time duration, in milliseconds. This - // duration is ideal for subtle animations or animations that occur - // very frequently. - private var shortAnimationDuration: Int = 0 - - private var setUpFullCover = false - - var fullRes:Drawable? = null - - private val dateFormat: DateFormat by lazy { - preferences.dateFormat().getOrDefault() - } - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - - override fun createPresenter(): MangaInfoPresenter { - val ctrl = parentController as MangaController - return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.manga_info_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - setUpFullCover = false - // Set onclickListener to toggle favorite when FAB clicked. - fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } - - // Set onLongClickListener to manage categories when FAB is clicked. - fab_favorite.longClicks().subscribeUntilDestroy { onFabLongClick() } - - // Set SwipeRefresh to refresh manga data. - swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } - - manga_full_title.longClicks().subscribeUntilDestroy { - copyToClipboard(view.context.getString(R.string.title), manga_full_title.text - .toString(), R.string.manga_info_full_title_label) - } - - manga_full_title.clicks().subscribeUntilDestroy { - performGlobalSearch(manga_full_title.text.toString()) - } - - manga_artist.longClicks().subscribeUntilDestroy { - copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString(), R - .string.manga_info_artist_label) - } - - manga_artist.clicks().subscribeUntilDestroy { - performGlobalSearch(manga_artist.text.toString()) - } - - manga_author.longClicks().subscribeUntilDestroy { - copyToClipboard(manga_author.text.toString(), manga_author.text.toString(), R.string - .manga_info_author_label) - } - - manga_author.clicks().subscribeUntilDestroy { - performGlobalSearch(manga_author.text.toString()) - } - - manga_summary.longClicks().subscribeUntilDestroy { - copyToClipboard(view.context.getString(R.string.description), manga_summary.text - .toString(), R.string.description) - } - - manga_genres_tags.setOnTagClickListener { tag -> performLocalSearch(tag) } - - manga_cover.clicks().subscribeUntilDestroy { - if (manga_cover.drawable != null) zoomImageFromThumb(manga_cover, manga_cover.drawable) - } - - // Retrieve and cache the system's default "short" animation time. - shortAnimationDuration = resources?.getInteger(android.R.integer.config_shortAnimTime) ?: 0 - - manga_cover.longClicks().subscribeUntilDestroy { - copyToClipboard(view.context.getString(R.string.title), presenter.manga.currentTitle(), R.string - .manga_info_full_title_label) - } - container = (view as ViewGroup).findViewById(R.id.manga_info_layout) as? View - val bottomM = manga_genres_tags.marginBottom - val fabBaseMarginBottom = fab_favorite.marginBottom - val mangaCoverMarginBottom = manga_cover.marginBottom - val fullMarginBottom = manga_cover_full?.marginBottom ?: 0 - container?.doOnApplyWindowInsets { v, insets, padding -> - if (resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) { - fab_favorite?.updateLayoutParams { - bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom - } - manga_cover?.updateLayoutParams { - bottomMargin = mangaCoverMarginBottom + insets.systemWindowInsetBottom - } - } - else { - manga_genres_tags?.updateLayoutParams { - bottomMargin = bottomM + insets.systemWindowInsetBottom - } - } - manga_cover_full?.updateLayoutParams { - bottomMargin = fullMarginBottom + insets.systemWindowInsetBottom - } - setFullCoverToThumb() - } - info_scrollview.doOnApplyWindowInsets { v, insets, padding -> - if (resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) { - v.updatePaddingRelative( - bottom = max(padding.bottom, insets.systemWindowInsetBottom) - ) - } - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.manga_info, menu) - - val editItem = menu.findItem(R.id.action_edit) - editItem.isVisible = presenter.manga.favorite - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_edit -> EditMangaDialog(this, presenter.manga).showDialog(router) - R.id.action_open_in_web_view -> openInWebView() - R.id.action_share -> prepareToShareManga() - R.id.action_add_to_home_screen -> addToHomeScreen() - } - return super.onOptionsItemSelected(item) - } - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextManga(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source) - - } else { - // Initialize manga. - fetchMangaFromSource() - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - val view = view ?: return - - //update full title TextView. - manga_full_title.text = if (manga.currentTitle().isBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentTitle() - } - - // Update artist TextView. - manga_artist.text = if (manga.currentArtist().isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentArtist() - } - - // Update author TextView. - manga_author.text = if (manga.currentAuthor().isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentAuthor() - } - - // If manga source is known update source TextView. - manga_source.text = source?.toString() ?: view.context.getString(R.string.unknown) - - // Update genres list - if (manga.currentGenres().isNullOrBlank().not()) { - manga_genres_tags.setTags(manga.currentGenres()?.split(", ")) - } - else manga_genres_tags.setTags(emptyList()) - - // Update description TextView. - manga_summary.text = if (manga.currentDesc().isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.currentDesc() - } - - // Update status TextView. - manga_status.setText(when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - }) - - // Set the favorite drawable to the correct one. - setFavoriteDrawable(manga.favorite) - activity?.invalidateOptionsMenu() - - // Set cover if it wasn't already. - if (!manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - //.centerCrop() - .into(manga_cover) - if (manga_cover_full != null) { - GlideApp.with(view.context).asDrawable().load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .override(CustomTarget.SIZE_ORIGINAL, CustomTarget.SIZE_ORIGINAL) - .into(object : CustomTarget() { - override fun onResourceReady(resource: Drawable, - transition: Transition? - ) { - fullRes = resource - } - - override fun onLoadCleared(placeholder: Drawable?) { } - }) - } - - if (backdrop != null) { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .centerCrop() - .into(backdrop) - } - } - } - - override fun onDestroyView(view: View) { - manga_genres_tags.setOnTagClickListener(null) - snack?.dismiss() - super.onDestroyView(view) - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Float) { - if (count > 0f) { - manga_chapters?.text = DecimalFormat("#.#").format(count) - } else { - manga_chapters?.text = resources?.getString(R.string.unknown) - } - } - - fun setLastUpdateDate(date: Date) { - if (date.time != 0L) { - manga_last_update?.text = dateFormat.format(date) - } else { - manga_last_update?.text = resources?.getString(R.string.unknown) - } - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - private fun toggleFavorite() { - presenter.toggleFavorite() - } - - private fun openInWebView() { - val source = presenter.source as? HttpSource ?: return - - val url = try { - source.mangaDetailsRequest(presenter.manga).url.toString() - } catch (e: Exception) { - return - } - - val activity = activity ?: return - val intent = WebViewActivity.newIntent(activity, source.id, url, presenter.manga.originalTitle()) - startActivity(intent) - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - private fun prepareToShareManga() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && manga_cover.drawable != null) - GlideApp.with(activity!!).asBitmap().load(presenter.manga).into(object : - CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - presenter.shareManga(resource) - } - override fun onLoadCleared(placeholder: Drawable?) {} - - override fun onLoadFailed(errorDrawable: Drawable?) { - shareManga() - } - }) - else shareManga() - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - fun shareManga(cover: File? = null) { - val context = view?.context ?: return - - val source = presenter.source as? HttpSource ?: return - val stream = cover?.getUriCompat(context) - try { - val url = source.mangaDetailsRequest(presenter.manga).url.toString() - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/*" - putExtra(Intent.EXTRA_TEXT, url) - putExtra(Intent.EXTRA_TITLE, presenter.manga.currentTitle()) - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - if (stream != null) { - clipData = ClipData.newRawUri(null, stream) - } - } - startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteDrawable(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fab_favorite?.setImageResource(if (isFavorite) - R.drawable.ic_bookmark_white_24dp - else - R.drawable.ic_add_to_library_24dp) - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaFromSource() { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource() - } - - - /** - * Update swipe refresh to stop showing refresh in progress spinner. - */ - fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - fun onFetchMangaError(error: Throwable) { - setRefreshing(false) - activity?.toast(error.message) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - private fun setRefreshing(value: Boolean) { - swipe_refresh?.isRefreshing = value - } - - /** - * Called when the fab is clicked. - */ - private fun onFabClick() { - val manga = presenter.manga - toggleFavorite() - if (manga.favorite) { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - when { - defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) - defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category - presenter.moveMangaToCategory(manga, null) - else -> { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - showAddedSnack() - } else { - showRemovedSnack() - } - } - - private fun showAddedSnack() { - val view = container - snack?.dismiss() - snack = view?.snack(view.context.getString(R.string.manga_added_library)) - } - - private fun showRemovedSnack() { - val view = container - snack?.dismiss() - if (view != null) { - snack = view.snack(view.context.getString(R.string.manga_removed_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_undo) { - presenter.setFavorite(true) - } - addCallback(object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (!presenter.manga.favorite) - presenter.confirmDeletion() - } - }) - } - (activity as? MainActivity)?.setUndoSnackBar(snack, fab_favorite) - } - } - - /** - * Called when the fab is long clicked. - */ - private fun onFabLongClick() { - val manga = presenter.manga - if (!manga.favorite) { - toggleFavorite() - showAddedSnack() - } - val categories = presenter.getCategories() - if (categories.isEmpty()) { - // no categories exist, display a message about adding categories - snack = container?.snack(R.string.action_add_category) - } else { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - - override fun updateCategoriesForMangas(mangas: List, categories: List) { - val manga = mangas.firstOrNull() ?: return - presenter.moveMangaToCategories(manga, categories) - } - - /** - * Add a shortcut of the manga to the home screen - */ - private fun addToHomeScreen() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // TODO are transformations really unsupported or is it just the Pixel Launcher? - createShortcutForShape() - } else { - ChooseShapeDialog(this).showDialog(router) - } - } - - /** - * Dialog to choose a shape for the icon. - */ - private class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) { - - constructor(target: MangaInfoController) : this() { - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val modes = intArrayOf(R.string.circular_icon, - R.string.rounded_icon, - R.string.square_icon, - R.string.star_icon) - - return MaterialDialog(activity!!) - .title(R.string.icon_shape) - .negativeButton(android.R.string.cancel) - .listItemsSingleChoice ( - items = modes.map { activity?.getString(it) as CharSequence }, - waitForPositiveButton = false) - { _, i, _ -> - (targetController as? MangaInfoController)?.createShortcutForShape(i) - dismissDialog() - } - } - } - - /** - * Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when - * the resource is available. - * - * @param i The shape index to apply. Defaults to circle crop transformation. - */ - private fun createShortcutForShape(i: Int = 0) { - if (activity == null) return - GlideApp.with(activity!!) - .asBitmap() - .load(presenter.manga) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .apply { - when (i) { - 0 -> circleCrop() - 1 -> transform(RoundedCorners(5)) - 2 -> transform(CropSquareTransformation()) - 3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star)) - } - } - .into(object : CustomTarget(96, 96) { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - createShortcut(resource) - } - - override fun onLoadCleared(placeholder: Drawable?) { } - - override fun onLoadFailed(errorDrawable: Drawable?) { - activity?.toast(R.string.icon_creation_fail) - } - }) - } - - /** - * Copies a string to clipboard - * - * @param label Label to show to the user describing the content - * @param content the actual text to copy to the board - */ - private fun copyToClipboard(label: String, content: String, resId: Int) { - if (content.isBlank()) return - - val activity = activity ?: return - val view = view ?: return - - val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText(label, content)) - - snack = container?.snack(view.context.getString(R.string.copied_to_clipboard, view.context - .getString(resId))) - } - - /** - * Perform a global search using the provided query. - * - * @param query the search query to pass to the search controller - */ - private fun performGlobalSearch(query: String) { - val router = parentController?.router ?: return - router.pushController(CatalogueSearchController(query).withFadeTransaction()) - } - - /** - * Perform a local search using the provided query. - * - * @param query the search query to pass to the library controller - */ - private fun performLocalSearch(query: String) { - val router = parentController?.router ?: return - val firstController = router.backstack.first()?.controller() - if (firstController is LibraryController && router.backstack.size == 2) { - router.handleBack() - firstController.search(query) - } - } - - /** - * Create shortcut using ShortcutManager. - * - * @param icon The image of the shortcut. - */ - private fun createShortcut(icon: Bitmap) { - val activity = activity ?: return - val mangaControllerArgs = parentController?.args ?: return - - // Create the shortcut intent. - val shortcutIntent = activity.intent - .setAction(MainActivity.SHORTCUT_MANGA) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaController.MANGA_EXTRA, - mangaControllerArgs.getLong(MangaController.MANGA_EXTRA)) - - // Check if shortcut placement is supported - if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { - val shortcutId = "manga-shortcut-${presenter.manga.originalTitle()}-${presenter.source.name}" - - // Create shortcut info - val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId) - .setShortLabel(presenter.manga.currentTitle()) - .setIcon(IconCompat.createWithBitmap(icon)) - .setIntent(shortcutIntent) - .build() - - val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create the CallbackIntent. - val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo) - - // Configure the intent so that the broadcast receiver gets the callback successfully. - PendingIntent.getBroadcast(activity, 0, intent, 0) - } else { - NotificationReceiver.shortcutCreatedBroadcast(activity) - } - - // Request shortcut. - ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo, - successCallback.intentSender) - } - } - - fun updateTitle() { - setMangaInfo(presenter.manga, presenter.source) - (parentController as? MangaController)?.updateTitle(presenter.manga) - } - - private fun setFullCoverToThumb() { - if (setUpFullCover) return - val expandedImageView = manga_cover_full ?: return - val thumbView = manga_cover - expandedImageView.pivotX = 0f - expandedImageView.pivotY = 0f - - val layoutParams = expandedImageView.layoutParams - layoutParams.height = thumbView.height - layoutParams.width = thumbView.width - expandedImageView.layoutParams = layoutParams - expandedImageView.scaleType = ImageView.ScaleType.FIT_CENTER - setUpFullCover = thumbView.height > 0 - } - - override fun handleBack(): Boolean { - if (manga_cover_full?.visibility == View.VISIBLE && - (parentController as? MangaController)?.tabLayout()?.selectedTabPosition == 0) - { - manga_cover_full?.performClick() - return true - } - return super.handleBack() - } - - private fun zoomImageFromThumb(thumbView: ImageView, cover: Drawable) { - // If there's an animation in progress, cancel it immediately and proceed with this one. - currentAnimator?.cancel() - - // Load the high-resolution "zoomed-in" image. - val expandedImageView = manga_cover_full ?: return - val fullBackdrop = full_backdrop - val image = fullRes ?: return - expandedImageView.setImageDrawable(image) - - // Hide the thumbnail and show the zoomed-in view. When the animation - // begins, it will position the zoomed-in view in the place of the - // thumbnail. - thumbView.alpha = 0f - expandedImageView.visibility = View.VISIBLE - fullBackdrop.visibility = View.VISIBLE - - // Set the pivot point to 0 to match thumbnail - - swipe_refresh.isEnabled = false - - val layoutParams = expandedImageView.layoutParams - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - expandedImageView.layoutParams = layoutParams - - // TransitionSet for the full cover because using animation for this SUCKS - val transitionSet = TransitionSet() - val bound = ChangeBounds() - transitionSet.addTransition(bound) - val changeImageTransform = ChangeImageTransform() - transitionSet.addTransition(changeImageTransform) - transitionSet.duration = shortAnimationDuration.toLong() - TransitionManager.beginDelayedTransition(manga_info_layout, transitionSet) - - // AnimationSet for backdrop because idk how to use TransitionSet - currentAnimator = AnimatorSet().apply { - play( - ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f, 0.5f) - ) - duration = shortAnimationDuration.toLong() - interpolator = DecelerateInterpolator() - addListener(object : AnimatorListenerAdapter() { - - override fun onAnimationEnd(animation: Animator) { - TransitionManager.endTransitions(manga_info_layout) - currentAnimator = null - } - - override fun onAnimationCancel(animation: Animator) { - TransitionManager.endTransitions(manga_info_layout) - currentAnimator = null - } - }) - start() - } - - expandedImageView.setOnClickListener { - currentAnimator?.cancel() - - val layoutParams = expandedImageView.layoutParams - layoutParams.height = thumbView.height - layoutParams.width = thumbView.width - expandedImageView.layoutParams = layoutParams - - // Zoom out back to tc thumbnail - val transitionSet = TransitionSet() - val bound = ChangeBounds() - transitionSet.addTransition(bound) - val changeImageTransform = ChangeImageTransform() - transitionSet.addTransition(changeImageTransform) - transitionSet.duration = shortAnimationDuration.toLong() - TransitionManager.beginDelayedTransition(manga_info_layout, transitionSet) - - // Animation to remove backdrop and hide the full cover - currentAnimator = AnimatorSet().apply { - play(ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f)) - duration = shortAnimationDuration.toLong() - interpolator = DecelerateInterpolator() - addListener(object : AnimatorListenerAdapter() { - - override fun onAnimationEnd(animation: Animator) { - thumbView.alpha = 1f - expandedImageView.visibility = View.GONE - fullBackdrop.visibility = View.GONE - swipe_refresh.isEnabled = true - currentAnimator = null - } - - override fun onAnimationCancel(animation: Animator) { - thumbView.alpha = 1f - expandedImageView.visibility = View.GONE - fullBackdrop.visibility = View.GONE - swipe_refresh.isEnabled = true - currentAnimator = null - } - }) - start() - } - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt deleted file mode 100644 index 43e6353e5a..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ /dev/null @@ -1,290 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.app.Application -import android.graphics.Bitmap -import android.net.Uri -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.storage.DiskUtil -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.util.Date - -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -class MangaInfoPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val coverCache: CoverCache = Injekt.get() -) : BasePresenter() { - - /** - * Subscription to send the manga to the view. - */ - private var viewMangaSubscription: Subscription? = null - - /** - * Subscription to update the manga from the source. - */ - private var fetchMangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - sendMangaToView() - - // Update chapter count - chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setChapterCount) - - // Update favorite status - mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribe { setFavorite(it) } - .apply { add(this) } - - //update last update date - lastUpdateRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setLastUpdateDate) - } - - /** - * Sends the active manga to the view. - */ - fun sendMangaToView() { - viewMangaSubscription?.let { remove(it) } - viewMangaSubscription = Observable.just(manga) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) - } - - /** - * Fetch manga information from source. - */ - fun fetchMangaFromSource() { - if (!fetchMangaSubscription.isNullOrUnsubscribed()) return - fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } - .map { networkManga -> - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).executeAsBlocking() - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - manga - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { sendMangaToView() } - .subscribeFirst({ view, _ -> - view.onFetchMangaDone() - }, MangaInfoController::onFetchMangaError) - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - * - * @return the new status of the manga. - */ - fun toggleFavorite(): Boolean { - manga.favorite = !manga.favorite - db.insertManga(manga).executeAsBlocking() - sendMangaToView() - return manga.favorite - } - - fun confirmDeletion() { - coverCache.deleteFromCache(manga.thumbnail_url) - db.resetMangaInfo(manga).executeAsBlocking() - downloadManager.deleteManga(manga, source) - } - - fun setFavorite(favorite: Boolean) { - if (manga.favorite == favorite) { - return - } - toggleFavorite() - } - - fun shareManga(cover: Bitmap) { - val context = Injekt.get() - - val destDir = File(context.cacheDir, "shared_image") - - Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file - .map { saveImage(cover, destDir, manga) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, file -> view.shareManga(file) }, - { view, error -> view.shareManga() } - ) - } - - private fun saveImage(cover:Bitmap, directory: File, manga: Manga): File? { - directory.mkdirs() - - // Build destination file. - val filename = DiskUtil.buildValidFilename("${manga.originalTitle()} - Cover.jpg") - - val destFile = File(directory, filename) - val stream: OutputStream = FileOutputStream(destFile) - cover.compress(Bitmap.CompressFormat.JPEG, 75, stream) - stream.flush() - stream.close() - return destFile - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - fun getCategories(): List { - return db.getCategories().executeAsBlocking() - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param manga the manga to move. - * @param categories the selected categories. - */ - fun moveMangaToCategories(manga: Manga, categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param manga the manga to move. - * @param category the selected category, or null for default category. - */ - fun moveMangaToCategory(manga: Manga, category: Category?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - - fun updateManga(title:String?, author:String?, artist: String?, uri: Uri?, - description: String?, tags: Array?) { - if (manga.source == LocalSource.ID) { - manga.title = if (title.isNullOrBlank()) manga.url else title.trim() - manga.author = author?.trim() - manga.artist = artist?.trim() - manga.description = description?.trim() - val tagsString = tags?.joinToString(", ") { it.capitalize() } - manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim() - LocalSource(downloadManager.context).updateMangaInfo(manga) - db.updateMangaInfo(manga).executeAsBlocking() - } - else { - var changed = false - val title = title?.trim() - if (!title.isNullOrBlank() && manga.originalTitle().isBlank()) { - manga.title = title - changed = true - } - else if (title.isNullOrBlank() && manga.currentTitle() != manga.originalTitle()) { - manga.title = manga.originalTitle() - changed = true - } else if (!title.isNullOrBlank() && title != manga.currentTitle()) { - manga.title = "${title}${SManga.splitter}${manga.originalTitle()}" - changed = true - } - - val author = author?.trim() - if (author.isNullOrBlank() && manga.currentAuthor() != manga.originalAuthor()) { - manga.author = manga.originalAuthor() - changed = true - } else if (!author.isNullOrBlank() && author != manga.currentAuthor()) { - manga.author = "${author}${SManga.splitter}${manga.originalAuthor() ?: ""}" - changed = true - } - - val artist = artist?.trim() - if (artist.isNullOrBlank() && manga.currentArtist() != manga.originalArtist()) { - manga.artist = manga.originalArtist() - changed = true - } else if (!artist.isNullOrBlank() && artist != manga.currentArtist()) { - manga.artist = "${artist}${SManga.splitter}${manga.originalArtist() ?: ""}" - changed = true - } - - val description = description?.trim() - if (description.isNullOrBlank() && manga.currentDesc() != manga.originalDesc()) { - manga.description = manga.originalDesc() - changed = true - } else if (!description.isNullOrBlank() && description != manga.currentDesc()) { - manga.description = "${description}${SManga.splitter}${manga.originalDesc() ?: ""}" - changed = true - } - - var tagsString = tags?.joinToString(", ") - if ((tagsString.isNullOrBlank() && manga.currentGenres() != manga.originalGenres()) - || tagsString == manga.originalGenres()) { - manga.genre = manga.originalGenres() - changed = true - } else if (!tagsString.isNullOrBlank() && tagsString != manga.currentGenres()) { - tagsString = tags?.joinToString(", ") { it.capitalize() } - manga.genre = "${tagsString}${SManga.splitter}${manga.originalGenres() ?: ""}" - changed = true - } - if (changed) db.updateMangaInfo(manga).executeAsBlocking() - } - if (uri != null) editCoverWithStream(uri) - - } - - private fun editCoverWithStream(uri: Uri): Boolean { - val inputStream = downloadManager.context.contentResolver.openInputStream(uri) ?: - return false - if (manga.source == LocalSource.ID) { - LocalSource.updateCover(downloadManager.context, manga, inputStream) - return true - } - - if (manga.thumbnail_url != null && manga.favorite) { - Injekt.get().refreshCoversToo().set(false) - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - return true - } - return false - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt index dd3f65d98f..ef606dd0bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt @@ -6,7 +6,6 @@ import android.widget.NumberPicker import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.getCustomView -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager @@ -15,14 +14,15 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SetTrackChaptersDialog : DialogController - where T : Controller, T : SetTrackChaptersDialog.Listener { + where T : SetTrackChaptersDialog.Listener { private val item: TrackItem + private lateinit var listener: Listener constructor(target: T, item: TrackItem) : super(Bundle().apply { putSerializable(KEY_ITEM_TRACK, item.track) }) { - targetController = target + listener = target this.item = item } @@ -45,16 +45,20 @@ class SetTrackChaptersDialog : DialogController // Remove focus to update selected number val np: NumberPicker = view.findViewById(R.id.chapters_picker) np.clearFocus() - (targetController as? Listener)?.setChaptersRead(item, np.value) + listener.setChaptersRead(item, np.value) } val view = dialog.getCustomView() val np: NumberPicker = view.findViewById(R.id.chapters_picker) // Set initial value np.value = item.track?.last_chapter_read ?: 0 - // Don't allow to go from 0 to 9999 - np.wrapSelectorWheel = false - + if (item.track?.total_chapters ?: 0 > 0) { + np.wrapSelectorWheel = true + np.maxValue = item.track?.total_chapters ?: 0 + } else { + // Don't allow to go from 0 to 9999 + np.wrapSelectorWheel = false + } return dialog } @@ -66,5 +70,4 @@ class SetTrackChaptersDialog : DialogController private companion object { const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track" } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt index 6aac10037e..ed67018864 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt @@ -6,7 +6,6 @@ import android.widget.NumberPicker import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.getCustomView -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager @@ -14,15 +13,15 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SetTrackScoreDialog : DialogController - where T : Controller, T : SetTrackScoreDialog.Listener { +class SetTrackScoreDialog : DialogController where T : SetTrackScoreDialog.Listener { private val item: TrackItem + private lateinit var listener: Listener constructor(target: T, item: TrackItem) : super(Bundle().apply { putSerializable(KEY_ITEM_TRACK, item.track) }) { - targetController = target + listener = target this.item = item } @@ -36,21 +35,17 @@ class SetTrackScoreDialog : DialogController override fun onCreateDialog(savedViewState: Bundle?): Dialog { val item = item - val dialog = MaterialDialog(activity!!) - .title(R.string.score) + val dialog = MaterialDialog(activity!!).title(R.string.score) .customView(R.layout.track_score_dialog, scrollable = false) - .negativeButton(android.R.string.cancel) - .positiveButton(android.R.string.ok) { dialog -> + .negativeButton(android.R.string.cancel).positiveButton(android.R.string.ok) { dialog -> val view = dialog.getCustomView() // Remove focus to update selected number val np: NumberPicker = view.findViewById(R.id.score_picker) np.clearFocus() - (targetController as? Listener)?.setScore(item, np.value) - + listener.setScore(item, np.value) } - val view = dialog.getCustomView() val np: NumberPicker = view.findViewById(R.id.score_picker) val scores = item.service.getScoreList().toTypedArray() @@ -74,5 +69,4 @@ class SetTrackScoreDialog : DialogController private companion object { const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track" } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt index 42ccd06a9f..dc621058b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt @@ -4,7 +4,6 @@ import android.app.Dialog import android.os.Bundle import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager @@ -13,14 +12,15 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SetTrackStatusDialog : DialogController - where T : Controller, T : SetTrackStatusDialog.Listener { + where T : SetTrackStatusDialog.Listener { private val item: TrackItem + private lateinit var listener: Listener constructor(target: T, item: TrackItem) : super(Bundle().apply { putSerializable(KEY_ITEM_TRACK, item.track) }) { - targetController = target + listener = target this.item = item } @@ -41,9 +41,8 @@ class SetTrackStatusDialog : DialogController .title(R.string.status) .negativeButton(android.R.string.cancel) .listItemsSingleChoice(items = statusString, initialSelection = selectedIndex, - waitForPositiveButton = false) - { dialog, position, _ -> - (targetController as? Listener)?.setStatus(item, position) + waitForPositiveButton = false) { dialog, position, _ -> + listener.setStatus(item, position) dialog.dismiss() } } @@ -55,5 +54,4 @@ class SetTrackStatusDialog : DialogController private companion object { const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index 4f4daf86c5..f4c6d54ff1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -1,11 +1,12 @@ package eu.kanade.tachiyomi.ui.manga.track -import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.util.view.inflate -class TrackAdapter(controller: TrackController) : RecyclerView.Adapter() { +class TrackAdapter(controller: OnClickListener) : RecyclerView.Adapter() { var items = emptyList() set(value) { @@ -34,12 +35,15 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter(), - TrackAdapter.OnClickListener, - SetTrackStatusDialog.Listener, - SetTrackChaptersDialog.Listener, - SetTrackScoreDialog.Listener { - - private var adapter: TrackAdapter? = null - - init { - // There's no menu, but this avoids a bug when coming from the catalogue, where the menu - // disappears if the searchview is expanded - setHasOptionsMenu(true) - } - - override fun createPresenter(): TrackPresenter { - return TrackPresenter((parentController as MangaController).manga!!) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.track_controller, container, false) - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - adapter = TrackAdapter(this) - with(view) { - track_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) - track_recycler.adapter = adapter - track_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - swipe_refresh.isEnabled = false - swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() } - } - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - fun onNextTrackings(trackings: List) { - val atLeastOneLink = trackings.any { it.track != null } - adapter?.items = trackings - swipe_refresh?.isEnabled = atLeastOneLink - (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) - } - - fun onSearchResults(results: List) { - getSearchDialog()?.onSearchResults(results) - } - - @Suppress("UNUSED_PARAMETER") - fun onSearchResultsError(error: Throwable) { - Timber.e(error) - getSearchDialog()?.onSearchResultsError() - } - - private fun getSearchDialog(): TrackSearchDialog? { - return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog - } - - fun onRefreshDone() { - swipe_refresh?.isRefreshing = false - } - - fun onRefreshError(error: Throwable) { - swipe_refresh?.isRefreshing = false - activity?.toast(error.message) - } - - override fun onLogoClick(position: Int) { - val track = adapter?.getItem(position)?.track ?: return - - if (track.tracking_url.isNullOrBlank()) { - activity?.toast(R.string.url_not_set) - } else { - activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url))) - } - } - - override fun onTitleClick(position: Int) { - val item = adapter?.getItem(position) ?: return - TrackSearchDialog(this, item.service, item.track != null).showDialog(router, - TAG_SEARCH_CONTROLLER) - } - - override fun onStatusClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackStatusDialog(this, item).showDialog(router) - } - - override fun onChaptersClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackChaptersDialog(this, item).showDialog(router) - } - - override fun onScoreClick(position: Int) { - val item = adapter?.getItem(position) ?: return - if (item.track == null) return - - SetTrackScoreDialog(this, item).showDialog(router) - } - - override fun setStatus(item: TrackItem, selection: Int) { - presenter.setStatus(item, selection) - swipe_refresh?.isRefreshing = true - } - - override fun setScore(item: TrackItem, score: Int) { - presenter.setScore(item, score) - swipe_refresh?.isRefreshing = true - } - - override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { - presenter.setLastChapterRead(item, chaptersRead) - swipe_refresh?.isRefreshing = true - } - - private companion object { - const val TAG_SEARCH_CONTROLLER = "track_search_controller" - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index c8c9da6887..472a60cd99 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -2,8 +2,11 @@ package eu.kanade.tachiyomi.ui.manga.track import android.annotation.SuppressLint import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.track_item.* class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { @@ -11,32 +14,48 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { init { val listener = adapter.rowClickListener logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) } - title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } - status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } - chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } + add_tracking.setOnClickListener { listener.onSetClick(adapterPosition) } + track_title.setOnClickListener { listener.onSetClick(adapterPosition) } + track_status.setOnClickListener { listener.onStatusClick(adapterPosition) } + track_chapters.setOnClickListener { listener.onChaptersClick(adapterPosition) } score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } } @SuppressLint("SetTextI18n") - @Suppress("DEPRECATION") fun bind(item: TrackItem) { val track = item.track track_logo.setImageResource(item.service.getLogo()) logo_container.setBackgroundColor(item.service.getLogoColor()) + logo_container.updateLayoutParams { + bottomToBottom = if (track != null) divider.id else track_details.id + } + track_logo.contentDescription = item.service.name + track_group.visibleIf(track != null) + add_tracking.visibleIf(track == null) if (track != null) { - track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary) - track_title.isAllCaps = false track_title.text = track.title - track_chapters.text = "${track.last_chapter_read}/" + - if (track.total_chapters > 0) track.total_chapters else "-" - track_status.text = item.service.getStatus(track.status) + with(track_chapters) { + text = when { + track.total_chapters > 0 && track.last_chapter_read == track.total_chapters -> + context.getString(R.string.all_chapters_read) + track.total_chapters > 0 -> context.getString( + R.string.chapter_x_of_y, track.last_chapter_read, track.total_chapters + ) + track.last_chapter_read > 0 -> context.getString( + R.string.chapter_, track.last_chapter_read + ) + else -> context.getString(R.string.not_started) + } + } + val status = item.service.getStatus(track.status) + if (status.isEmpty()) track_status.setText(R.string.unknown_status) + else track_status.text = item.service.getStatus(track.status) track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) - } else { - track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button) - track_title.setText(R.string.action_edit) - track_chapters.text = "" - track_score.text = "" - track_status.text = "" } } + + fun setProgress(enabled: Boolean) { + progress.visibleIf(enabled) + track_logo.visibleIf(!enabled) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt deleted file mode 100644 index 5978c758f1..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ /dev/null @@ -1,130 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.system.toast -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - - -class TrackPresenter( - val manga: Manga, - preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val trackManager: TrackManager = Injekt.get() -) : BasePresenter() { - - private val context = preferences.context - - private var trackList: List = emptyList() - - private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - - private var trackSubscription: Subscription? = null - - private var searchSubscription: Subscription? = null - - private var refreshSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - fetchTrackings() - } - - fun fetchTrackings() { - trackSubscription?.let { remove(it) } - trackSubscription = db.getTracks(manga) - .asRxObservable() - .map { tracks -> - loggedServices.map { service -> - TrackItem(tracks.find { it.sync_id == service.id }, service) - } - } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { trackList = it } - .subscribeLatestCache(TrackController::onNextTrackings) - } - - fun refresh() { - refreshSubscription?.let { remove(it) } - refreshSubscription = Observable.from(trackList) - .filter { it.track != null } - .concatMap { item -> - item.service.refresh(item.track!!) - .flatMap { db.insertTrack(it).asRxObservable() } - .map { item } - .onErrorReturn { item } - } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - TrackController::onRefreshError) - } - - fun search(query: String, service: TrackService) { - searchSubscription?.let { remove(it) } - searchSubscription = service.search(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(TrackController::onSearchResults, - TrackController::onSearchResultsError) - } - - fun registerTracking(item: Track?, service: TrackService) { - if (item != null) { - item.manga_id = manga.id!! - add(service.bind(item) - .flatMap { db.insertTrack(item).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - TrackController::onRefreshError)) - } else { - db.deleteTrackForManga(manga, service).executeAsBlocking() - } - } - - private fun updateRemote(track: Track, service: TrackService) { - service.update(track) - .flatMap { db.insertTrack(track).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> view.onRefreshDone() }, - { view, error -> - view.onRefreshError(error) - - // Restart on error to set old values - fetchTrackings() - }) - } - - fun setStatus(item: TrackItem, index: Int) { - val track = item.track!! - track.status = item.service.getStatusList()[index] - updateRemote(track, item.service) - } - - fun setScore(item: TrackItem, index: Int) { - val track = item.track!! - track.score = item.service.indexToScore(index) - updateRemote(track, item.service) - } - - fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { - val track = item.track!! - track.last_chapter_read = chapterNumber - updateRemote(track, item.service) - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt index 47a57f090e..e5a0c5966c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt @@ -5,16 +5,17 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.inflate import kotlinx.android.synthetic.main.track_search_item.view.* -import java.util.* +import java.util.ArrayList -class TrackSearchAdapter(context: Context) - : ArrayAdapter(context, R.layout.track_search_item, ArrayList()) { +class TrackSearchAdapter(context: Context) : + ArrayAdapter(context, R.layout.track_search_item, ArrayList()) { override fun getView(position: Int, view: View?, parent: ViewGroup): View { var v = view @@ -47,11 +48,10 @@ class TrackSearchAdapter(context: Context) view.track_search_summary.text = track.summary GlideApp.with(view.context).clear(view.track_search_cover) if (!track.cover_url.isNullOrEmpty()) { - GlideApp.with(view.context) - .load(track.cover_url) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(view.track_search_cover) + GlideApp.with(view.context).load(track.cover_url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop() + .transition(DrawableTransitionOptions.withCrossFade()) + .into(view.track_search_cover) } if (track.publishing_status.isNullOrBlank()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index e6f6337f10..0a555cbab5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -15,11 +15,9 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.ui.base.controller.DialogController -import kotlinx.android.synthetic.main.track_controller.* +import eu.kanade.tachiyomi.ui.manga.MangaDetailsPresenter import eu.kanade.tachiyomi.util.lang.plusAssign -import kotlinx.android.synthetic.main.track_search_dialog.view.progress -import kotlinx.android.synthetic.main.track_search_dialog.view.track_search -import kotlinx.android.synthetic.main.track_search_dialog.view.track_search_list +import kotlinx.android.synthetic.main.track_search_dialog.view.* import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription @@ -41,17 +39,18 @@ class TrackSearchDialog : DialogController { private var searchTextSubscription: Subscription? = null - private val trackController - get() = targetController as TrackController + private lateinit var bottomSheet: TrackingBottomSheet - private var wasPreviouslyTracked:Boolean = false + private var wasPreviouslyTracked: Boolean = false + private lateinit var presenter: MangaDetailsPresenter - constructor(target: TrackController, service: TrackService, wasTracked:Boolean) : super(Bundle() + constructor(target: TrackingBottomSheet, service: TrackService, wasTracked: Boolean) : super(Bundle() .apply { - putInt(KEY_SERVICE, service.id) - }) { + putInt(KEY_SERVICE, service.id) + }) { wasPreviouslyTracked = wasTracked - targetController = target + bottomSheet = target + presenter = target.presenter this.service = service } @@ -64,9 +63,7 @@ class TrackSearchDialog : DialogController { val dialog = MaterialDialog(activity!!).apply { customView(viewRes = R.layout.track_search_dialog, scrollable = false) negativeButton(android.R.string.cancel) - positiveButton( - if (wasPreviouslyTracked) R.string.action_clear - else R.string.action_track){ onPositiveButtonClick() } + positiveButton(R.string.clear) { onPositiveButtonClick() } setActionButtonEnabled(WhichButton.POSITIVE, wasPreviouslyTracked) } @@ -90,19 +87,24 @@ class TrackSearchDialog : DialogController { selectedItem = null subscriptions += view.track_search_list.itemClicks().subscribe { position -> - selectedItem = adapter.getItem(position) - (dialog as? MaterialDialog)?.positiveButton(R.string.action_track) - (dialog as? MaterialDialog)?.setActionButtonEnabled(WhichButton.POSITIVE, true) + trackItem(position) } // Do an initial search based on the manga's title if (savedState == null) { - val title = trackController.presenter.manga.originalTitle() + val title = presenter.manga.title view.track_search.append(title) search(title) } } + private fun trackItem(position: Int) { + selectedItem = adapter?.getItem(position) + bottomSheet.refreshTrack(service) + presenter.registerTracking(selectedItem, service) + dismissDialog() + } + override fun onDestroyView(view: View) { super.onDestroyView(view) subscriptions.unsubscribe() @@ -129,7 +131,7 @@ class TrackSearchDialog : DialogController { val view = dialogView ?: return view.progress.visibility = View.VISIBLE view.track_search_list.visibility = View.INVISIBLE - trackController.presenter.search(query, service) + presenter.trackSearch(query, service) } fun onSearchResults(results: List) { @@ -139,9 +141,7 @@ class TrackSearchDialog : DialogController { view.track_search_list.visibility = View.VISIBLE adapter?.setItems(results) if (results.size == 1 && !wasPreviouslyTracked) { - selectedItem = adapter?.getItem(0) - (dialog as? MaterialDialog)?.positiveButton(R.string.action_track) - (dialog as? MaterialDialog)?.setActionButtonEnabled(WhichButton.POSITIVE, true) + trackItem(0) } } @@ -153,12 +153,12 @@ class TrackSearchDialog : DialogController { } private fun onPositiveButtonClick() { - trackController.swipe_refresh.isRefreshing = true - trackController.presenter.registerTracking(selectedItem, service) + bottomSheet.refreshTrack(service) + presenter.registerTracking(null, + service) } private companion object { const val KEY_SERVICE = "service_id" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt new file mode 100644 index 0000000000..2302f24009 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt @@ -0,0 +1,183 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import kotlinx.android.synthetic.main.tracking_bottom_sheet.* +import timber.log.Timber + +class TrackingBottomSheet(private val controller: MangaDetailsController) : BottomSheetDialog + (controller.activity!!, R.style.BottomSheetDialogTheme), + TrackAdapter.OnClickListener, + SetTrackStatusDialog.Listener, + SetTrackChaptersDialog.Listener, + SetTrackScoreDialog.Listener { + + val activity = controller.activity!! + + private var sheetBehavior: BottomSheetBehavior<*> + + val presenter = controller.presenter + + private var adapter: TrackAdapter? = null + + init { + // Use activity theme for this layout + val view = activity.layoutInflater.inflate(R.layout.tracking_bottom_sheet, null) + setContentView(view) + + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) + setEdgeToEdge(activity, view) + val height = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + sheetBehavior.peekHeight = 380.dpToPx + height + + sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { } + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.skipCollapsed = true + } + } + }) + } + + override fun onStart() { + super.onStart() + sheetBehavior.skipCollapsed = true + } + + /** + * Called when the sheet is created. It initializes the listeners and values of the preferences. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + adapter = TrackAdapter(this) + track_recycler.layoutManager = LinearLayoutManager(context) + track_recycler.adapter = adapter + track_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + + adapter?.items = presenter.trackList + } + + fun onNextTrackings(trackings: List) { + onRefreshDone() + adapter?.items = trackings + controller.refreshTracker() + } + + fun onSearchResults(results: List) { + getSearchDialog()?.onSearchResults(results) + } + + fun onSearchResultsError(error: Throwable) { + Timber.e(error) + activity.toast(error.message) + getSearchDialog()?.onSearchResultsError() + } + + private fun getSearchDialog(): TrackSearchDialog? { + return controller.router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog + } + + fun onRefreshDone() { + for (i in adapter!!.items.indices) { + (track_recycler.findViewHolderForAdapterPosition(i) as? TrackHolder)?.setProgress(false) + } + } + + fun onRefreshError(error: Throwable) { + for (i in adapter!!.items.indices) { + (track_recycler.findViewHolderForAdapterPosition(i) as? TrackHolder)?.setProgress(false) + } + activity.toast(error.message) + } + + override fun onLogoClick(position: Int) { + val track = adapter?.getItem(position)?.track ?: return + + if (track.tracking_url.isBlank()) { + activity.toast(R.string.url_not_set_click_again) + } else { + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url))) + controller.refreshTracker = position + } + } + + override fun onSetClick(position: Int) { + val item = adapter?.getItem(position) ?: return + TrackSearchDialog(this, item.service, item.track != null).showDialog( + controller.router, + TAG_SEARCH_CONTROLLER + ) + } + + override fun onStatusClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackStatusDialog(this, item).showDialog(controller.router) + } + + override fun onChaptersClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackChaptersDialog(this, item).showDialog(controller.router) + } + + override fun onScoreClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackScoreDialog(this, item).showDialog(controller.router) + } + + override fun setStatus(item: TrackItem, selection: Int) { + presenter.setStatus(item, selection) + refreshItem(item) + } + + private fun refreshItem(item: TrackItem) { + refreshTrack(item.service) + } + + fun refreshItem(index: Int) { + (track_recycler.findViewHolderForAdapterPosition(index) as? TrackHolder)?.setProgress(true) + } + + fun refreshTrack(item: TrackService?) { + val index = adapter?.indexOf(item) ?: -1 + if (index > -1) { + (track_recycler.findViewHolderForAdapterPosition(index) as? TrackHolder) + ?.setProgress(true) + } + } + + override fun setScore(item: TrackItem, score: Int) { + presenter.setScore(item, score) + refreshItem(item) + } + + override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { + presenter.setLastChapterRead(item, chaptersRead) + refreshItem(item) + } + + private companion object { + const val TAG_SEARCH_CONTROLLER = "track_search_controller" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt index 0fb2c24f8a..a2860c2e58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt @@ -14,4 +14,4 @@ class MangaAdapter(controller: MigrationController) : super.updateDataSet(items) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt index 61a9a14199..9d09649efd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt @@ -1,38 +1,30 @@ package eu.kanade.tachiyomi.ui.migration import android.view.View +import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import kotlinx.android.synthetic.main.catalogue_list_item.* -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.items.IFlexible class MangaHolder( - private val view: View, - private val adapter: FlexibleAdapter> + private val view: View, + private val adapter: FlexibleAdapter> ) : BaseFlexibleViewHolder(view, adapter) { fun bind(item: MangaItem) { // Update the title of the manga. - title.text = item.manga.currentTitle() - - // Create thumbnail onclick to simulate long click - thumbnail.setOnClickListener { - // Simulate long click on this view to enter selection mode - onLongClick(itemView) - } + title.text = item.manga.title + subtitle.text = item.manga.author?.trim() // Update the cover. - GlideApp.with(itemView.context).clear(thumbnail) + GlideApp.with(itemView.context).clear(cover_thumbnail) GlideApp.with(itemView.context) .load(item.manga) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .circleCrop() .dontAnimate() - .into(thumbnail) + .into(cover_thumbnail) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt index 824a05fd8e..76636b7d8e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt @@ -18,10 +18,12 @@ class MangaItem(val manga: Manga) : AbstractFlexibleItem() { return MangaHolder(view, adapter) } - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: MangaHolder, - position: Int, - payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: MangaHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt index 3c709915de..d2a39e4bfd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt @@ -11,13 +11,11 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController -import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener import eu.kanade.tachiyomi.util.system.await import eu.kanade.tachiyomi.util.system.launchUI -import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController import kotlinx.android.synthetic.main.migration_controller.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -47,7 +45,7 @@ class MigrationController : NucleusController(), return inflater.inflate(R.layout.migration_controller, container, false) } - fun searchController(manga:Manga): SearchController { + fun searchController(manga: Manga): SearchController { val controller = SearchController(manga) controller.targetController = this @@ -56,6 +54,7 @@ class MigrationController : NucleusController(), override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForController() adapter = FlexibleAdapter(null, this) migration_recycler.layoutManager = @@ -84,19 +83,24 @@ class MigrationController : NucleusController(), fun render(state: ViewState) { if (state.selectedSource == null) { - title = resources?.getString(R.string.label_migration) + title = resources?.getString(R.string.source_migration) if (adapter !is SourceAdapter) { adapter = SourceAdapter(this) migration_recycler.adapter = adapter } adapter?.updateDataSet(state.sourcesWithManga) } else { + val switching = title == resources?.getString(R.string.source_migration) title = state.selectedSource.toString() if (adapter !is MangaAdapter) { adapter = MangaAdapter(this) migration_recycler.adapter = adapter } - adapter?.updateDataSet(state.mangaForSource) + adapter?.updateDataSet(state.mangaForSource, true) + /*if (switching) launchUI { + migration_recycler.alpha = 0f + migration_recycler.animate().alpha(1f).setStartDelay(100).setDuration(200).start() + }*/ } } @@ -104,10 +108,9 @@ class MigrationController : NucleusController(), val item = adapter?.getItem(position) ?: return false if (item is MangaItem) { - val controller = SearchController(item.manga) - controller.targetController = this - - router.pushController(controller.withFadeTransaction()) + PreMigrationController.navigateToMigration(Injekt.get().skipPreMigration().getOrDefault(), + router, + listOf(item.manga.id!!)) } else if (item is SourceItem) { presenter.setSelectedSource(item.source) } @@ -128,15 +131,9 @@ class MigrationController : NucleusController(), val sourceMangas = manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList() withContext(Dispatchers.Main) { - router.pushController( - if (Injekt.get().skipPreMigration().getOrDefault()) { - MigrationListController.create( - MigrationProcedureConfig(sourceMangas, null) - ) - } else { - PreMigrationController.create(sourceMangas) - }.withFadeTransaction() - ) + PreMigrationController.navigateToMigration(Injekt.get().skipPreMigration().getOrDefault(), + router, + sourceMangas) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt index 2e9f3aa8d9..adc20377b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt @@ -4,15 +4,15 @@ import eu.kanade.tachiyomi.R object MigrationFlags { - const val CHAPTERS = 0b001 + const val CHAPTERS = 0b001 const val CATEGORIES = 0b010 - const val TRACK = 0b100 + const val TRACK = 0b100 - private const val CHAPTERS2 = 0x1 + private const val CHAPTERS2 = 0x1 private const val CATEGORIES2 = 0x2 - private const val TRACK2 = 0x4 + private const val TRACK2 = 0x4 - val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track) + val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.tracking) val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt index 60f72ac836..965c09cb99 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt @@ -22,18 +22,18 @@ class MigrationMangaDialog(bundle: Bundle? = null) : DialogController(bundle) } override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val confirmRes = if (copy) R.plurals.confirm_copy else R.plurals.confirm_migration + val confirmRes = if (copy) R.plurals.copy_manga else R.plurals.migrate_manga val confirmString = applicationContext?.resources?.getQuantityString(confirmRes, mangaSet, mangaSet, ( - if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_x, mangaSkipped) + if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_, mangaSkipped) else "")) ?: "" return MaterialDialog(activity!!).show { message(text = confirmString) - positiveButton(if (copy) R.string.copy else R.string.migrate) { + positiveButton(if (copy) R.string.copy_value else R.string.migrate) { if (copy) (targetController as? MigrationListController)?.copyMangas() else (targetController as? MigrationListController)?.migrateMangas() } negativeButton(android.R.string.no) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt index 8d7b2cab1a..0c7e94fc63 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt @@ -21,9 +21,9 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class MigrationPresenter( - private val sourceManager: SourceManager = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get() + private val sourceManager: SourceManager = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() ) : BasePresenter() { var state = ViewState() @@ -37,27 +37,19 @@ class MigrationPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - db.getFavoriteMangas() - .asRxObservable() - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) } - .combineLatest(stateRelay.map { it.selectedSource } - .distinctUntilChanged() - ) { library, source -> library to source } - .filter { (_, source) -> source != null } - .observeOn(Schedulers.io()) - .map { (library, source) -> libraryToMigrationItem(library, source!!.id) } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { state = state.copy(mangaForSource = it) } - .subscribe() + db.getFavoriteMangas().asRxObservable().observeOn(AndroidSchedulers.mainThread()) + .doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) } + .combineLatest(stateRelay.map { it.selectedSource } + .distinctUntilChanged()) { library, source -> library to source } + .filter { (_, source) -> source != null }.observeOn(Schedulers.io()) + .map { (library, source) -> libraryToMigrationItem(library, source!!.id) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { state = state.copy(mangaForSource = it) }.subscribe() stateRelay - // Render the view when any field other than isReplacingManga changes - .distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga } - .subscribeLatestCache(MigrationController::render) - - /* stateRelay.distinctUntilChanged { state -> state.isReplacingManga } - .subscribeLatestCache(MigrationController::renderIsReplacingManga)*/ + // Render the view when any field other than isReplacingManga changes + .distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga } + .subscribeLatestCache(MigrationController::render) } fun setSelectedSource(source: Source) { @@ -71,8 +63,8 @@ class MigrationPresenter( private fun findSourcesWithManga(library: List): List { val header = SelectionHeader() return library.map { it.source }.toSet() - .mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null } - .map { SourceItem(it, header) } + .mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null } + .map { SourceItem(it, header) } } private fun libraryToMigrationItem(library: List, sourceId: Long): List { @@ -84,18 +76,20 @@ class MigrationPresenter( state = state.copy(isReplacingManga = true) - Observable.defer { source.fetchChapterList(manga) } - .onErrorReturn { emptyList() } - .doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) } - .onErrorReturn { emptyList() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnUnsubscribe { state = state.copy(isReplacingManga = false) } - .subscribe() + Observable.defer { source.fetchChapterList(manga) }.onErrorReturn { emptyList() } + .doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) } + .onErrorReturn { emptyList() }.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnUnsubscribe { state = state.copy(isReplacingManga = false) }.subscribe() } - private fun migrateMangaInternal(source: Source, sourceChapters: List, - prevManga: Manga, manga: Manga, replace: Boolean) { + private fun migrateMangaInternal( + source: Source, + sourceChapters: List, + prevManga: Manga, + manga: Manga, + replace: Boolean + ) { val flags = preferences.migrateFlags().getOrDefault() val migrateChapters = MigrationFlags.hasChapters(flags) @@ -112,8 +106,8 @@ class MigrationPresenter( } val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() - val maxChapterRead = prevMangaChapters.filter { it.read } - .maxBy { it.chapter_number }?.chapter_number + val maxChapterRead = + prevMangaChapters.filter { it.read }.maxBy { it.chapter_number }?.chapter_number if (maxChapterRead != null) { val dbChapters = db.getChapters(manga).executeAsBlocking() for (chapter in dbChapters) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt index dd1dc42a40..93efd4694e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt @@ -18,12 +18,13 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter +import eu.kanade.tachiyomi.ui.main.BottomNavBarInterface import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController import uy.kohesive.injekt.injectLazy class SearchController( - private var manga: Manga? = null -) : CatalogueSearchController(manga?.originalTitle()) { + private var manga: Manga? = null +) : CatalogueSearchController(manga?.title), BottomNavBarInterface { private var newManga: Manga? = null private var progress = 1 @@ -36,12 +37,10 @@ class SearchController( setHasOptionsMenu(true) } - override fun getTitle(): String? { if (totalProgress > 1) { return "($progress/$totalProgress) ${super.getTitle()}" - } - else + } else return super.getTitle() } @@ -100,7 +99,7 @@ class SearchController( private fun replaceWithNewSearchController(manga: Manga?) { if (manga != null) { - //router.popCurrentController() + // router.popCurrentController() val searchController = SearchController(manga) searchController.targetController = targetController searchController.progress = progress + 1 @@ -139,21 +138,20 @@ class SearchController( val preselected = MigrationFlags.getEnabledFlagsPositions(prefValue) return MaterialDialog(activity!!) - .message(R.string.migration_dialog_what_to_include) + .message(R.string.data_to_include_in_migration) .listItemsMultiChoice(items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence }, - initialSelection = preselected.toIntArray()) { _, positions, _ -> + initialSelection = preselected.toIntArray()) { _, positions, _ -> val newValue = MigrationFlags.getFlagsFromPositions(positions.toTypedArray()) preferences.migrateFlags().set(newValue) } .positiveButton(R.string.migrate) { (targetController as? SearchController)?.migrateManga() } - .negativeButton(R.string.copy) { + .negativeButton(R.string.copy_value) { (targetController as? SearchController)?.copyManga() } } - } /** @@ -191,5 +189,10 @@ class SearchController( } } - + override fun canChangeTabs(block: () -> Unit): Boolean { + val migrationListController = router.getControllerWithTag(MigrationListController.TAG) + as? BottomNavBarInterface + if (migrationListController != null) return migrationListController.canChangeTabs(block) + return true + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt index 941e99c2e8..78177dd944 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt @@ -2,14 +2,13 @@ package eu.kanade.tachiyomi.ui.migration import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter class SearchPresenter( - initialQuery: String? = "", - private val manga: Manga + initialQuery: String? = "", + private val manga: Manga ) : CatalogueSearchPresenter(initialQuery) { override fun getEnabledSources(): List { @@ -19,7 +18,7 @@ class SearchPresenter( } override fun createCatalogueSearchItem(source: CatalogueSource, results: List?): CatalogueSearchItem { - //Set the catalogue search item as highlighted if the source matches that of the selected manga + // Set the catalogue search item as highlighted if the source matches that of the selected manga return CatalogueSearchItem(source, results, source.id == manga.source) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt index d88f7bdf66..9d3131e02f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt @@ -31,14 +31,18 @@ class SelectionHeader : AbstractHeaderItem() { /** * Binds this item to the given view holder. */ - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, - position: Int, payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: Holder, + position: Int, + payloads: MutableList? + ) { // Intentionally empty } class Holder(view: View, adapter: FlexibleAdapter>) : BaseFlexibleViewHolder(view, adapter) { init { - title.text = view.context.getString(R.string.migration_selection_prompt) + title.text = view.context.getString(R.string.select_a_source_to_migrate_from) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt index d77d37f96f..b25fd604af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt @@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.ui.migration import android.view.View 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.getRound +import eu.kanade.tachiyomi.util.view.roundTextIcon import io.github.mthli.slice.Slice import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.* @@ -20,7 +21,7 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) : get() = card init { - source_latest.text = "Auto" + source_latest.text = view.context.getString(R.string.auto) source_browse.setText(R.string.select) source_browse.setOnClickListener { adapter.selectClickListener?.onSelectClick(adapterPosition) @@ -39,7 +40,9 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) : // Set circle letter image. itemView.post { - image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + val icon = source.icon() + if (icon != null) edit_button.setImageDrawable(source.icon()) + else edit_button.roundTextIcon(source.name) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt index 4d0b9f383b..6668d252dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt @@ -34,10 +34,13 @@ data class SourceItem(val source: Source, val header: SelectionHeader? = null) : /** * Binds this item to the given view holder. */ - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: SourceHolder, - position: Int, payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: SourceHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(this) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt index 7caa5e9ecc..0481ed6037 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt @@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.migration import eu.kanade.tachiyomi.source.Source data class ViewState( - val selectedSource: Source? = null, - val mangaForSource: List = emptyList(), - val sourcesWithManga: List = emptyList(), - val isReplacingManga: Boolean = false -) \ No newline at end of file + val selectedSource: Source? = null, + val mangaForSource: List = emptyList(), + val sourcesWithManga: List = emptyList(), + val isReplacingManga: Boolean = false +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationBottomSheetDialog.kt index f106024400..060f10cfc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationBottomSheetDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationBottomSheetDialog.kt @@ -8,6 +8,7 @@ import android.widget.LinearLayout import android.widget.RadioButton import android.widget.RadioGroup import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout import com.bluelinelabs.conductor.Controller import com.f2prateek.rx.preferences.Preference import com.google.android.material.bottomsheet.BottomSheetDialog @@ -15,36 +16,56 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.migration.MigrationFlags -import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge 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 -import kotlinx.android.synthetic.main.migration_bottom_sheet.mig_categories -import kotlinx.android.synthetic.main.migration_bottom_sheet.mig_chapters -import kotlinx.android.synthetic.main.migration_bottom_sheet.mig_tracking import uy.kohesive.injekt.injectLazy -class MigrationBottomSheetDialog(activity: Activity, theme: Int, private val listener: -StartMigrationListener) : - BottomSheetDialog(activity, - theme) { +class MigrationBottomSheetDialog( + activity: Activity, + private val listener: StartMigrationListener +) : BottomSheetDialog(activity, R.style.BottomSheetDialogTheme) { + /** * Preferences helper. */ private val preferences by injectLazy() init { - // Use activity theme for this layout val view = activity.layoutInflater.inflate(R.layout.migration_bottom_sheet, null) - //val scroll = NestedScrollView(context) - // scroll.addView(view) setContentView(view) - if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) + if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) { sourceGroup.orientation = LinearLayout.HORIZONTAL - window?.setBackgroundDrawable(null) + val params = skip_step.layoutParams as ConstraintLayout.LayoutParams + params.apply { + topToBottom = -1 + startToStart = -1 + bottomToBottom = extra_search_param.id + startToEnd = extra_search_param.id + endToEnd = sourceGroup.id + topToTop = extra_search_param.id + marginStart = 16.dpToPx + } + skip_step.layoutParams = params + + val params2 = extra_search_param_text.layoutParams as ConstraintLayout.LayoutParams + params2.bottomToBottom = options_layout.id + extra_search_param_text.layoutParams = params2 + + val params3 = extra_search_param.layoutParams as ConstraintLayout.LayoutParams + params3.endToEnd = -1 + extra_search_param.layoutParams = params3 + } + setEdgeToEdge(activity, view) + setBottomEdge( + if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) extra_search_param_text + else skip_step, activity + ) } /** @@ -55,11 +76,13 @@ StartMigrationListener) : initPreferences() + // window?.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + fab.setOnClickListener { preferences.skipPreMigration().set(skip_step.isChecked) listener.startMigration( - if (extra_search_param.isChecked && extra_search_param_text.text.isNotBlank()) - extra_search_param_text.text.toString() else null) + if (extra_search_param.isChecked && extra_search_param_text.text.isNotBlank()) extra_search_param_text.text.toString() else null + ) dismiss() } } @@ -90,17 +113,17 @@ StartMigrationListener) : skip_step.isChecked = preferences.skipPreMigration().getOrDefault() skip_step.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) - (listener as? Controller)?.activity?.toast(R.string.pre_migration_skip_toast, - Toast.LENGTH_LONG) + if (isChecked) (listener as? Controller)?.activity?.toast( + R.string.to_show_again_setting_library, Toast.LENGTH_LONG + ) } } private fun setFlags() { var flags = 0 - if(mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS - if(mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES - if(mig_tracking.isChecked) flags = flags or MigrationFlags.TRACK + if (mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS + if (mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES + if (mig_tracking.isChecked) flags = flags or MigrationFlags.TRACK preferences.migrateFlags().set(flags) } @@ -124,11 +147,8 @@ StartMigrationListener) : } private fun Boolean.toInt() = if (this) 1 else 0 - - - } interface StartMigrationListener { - fun startMigration(extraParam:String?) -} \ No newline at end of file + fun startMigration(extraParam: String?) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceAdapter.kt index f35b3a5db3..7056d18da2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceAdapter.kt @@ -5,9 +5,10 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.source.SourceManager import uy.kohesive.injekt.injectLazy -class MigrationSourceAdapter(var items: List, - val controllerPre: PreMigrationController -): FlexibleAdapter( +class MigrationSourceAdapter( + var items: List, + val controllerPre: PreMigrationController +) : FlexibleAdapter( items, controllerPre, true @@ -21,7 +22,7 @@ class MigrationSourceAdapter(var items: List, } override fun onRestoreInstanceState(savedInstanceState: Bundle) { - val sourceManager:SourceManager by injectLazy() + val sourceManager: SourceManager by injectLazy() savedInstanceState.getParcelableArrayList( SELECTED_SOURCES_KEY )?.let { @@ -38,4 +39,4 @@ class MigrationSourceAdapter(var items: List, companion object { private const val SELECTED_SOURCES_KEY = "selected_sources" } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceHolder.kt index 33417bb578..bb75df7181 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceHolder.kt @@ -4,13 +4,14 @@ import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG import android.view.View import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.util.view.getRound +import eu.kanade.tachiyomi.util.view.roundTextIcon import kotlinx.android.synthetic.main.migration_source_item.* import uy.kohesive.injekt.injectLazy -class MigrationSourceHolder(view: View, val adapter: MigrationSourceAdapter): +class MigrationSourceHolder(view: View, val adapter: MigrationSourceAdapter) : BaseFlexibleViewHolder(view, adapter) { init { setDragHandleView(reorder) @@ -24,16 +25,18 @@ class MigrationSourceHolder(view: View, val adapter: MigrationSourceAdapter): title.text = sourceName // Update circle letter image. itemView.post { - image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + val icon = source.icon() + if (icon != null) edit_button.setImageDrawable(source.icon()) + else edit_button.roundTextIcon(source.name) } - if(sourceEnabled) { + if (sourceEnabled) { title.alpha = 1.0f - image.alpha = 1.0f + edit_button.alpha = 1.0f title.paintFlags = title.paintFlags and STRIKE_THRU_TEXT_FLAG.inv() } else { title.alpha = DISABLED_ALPHA - image.alpha = DISABLED_ALPHA + edit_button.alpha = DISABLED_ALPHA title.paintFlags = title.paintFlags or STRIKE_THRU_TEXT_FLAG } } @@ -51,4 +54,4 @@ class MigrationSourceHolder(view: View, val adapter: MigrationSourceAdapter): companion object { private const val DISABLED_ALPHA = 0.3f } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceItem.kt index c01b40890e..de66151e41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationSourceItem.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.android.parcel.Parcelize -class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): AbstractFlexibleItem() { +class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean) : AbstractFlexibleItem() { override fun getLayoutRes() = R.layout.migration_source_item override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MigrationSourceHolder { @@ -26,10 +26,12 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): A * @param position The position of this item in the adapter. * @param payloads List of partial changes. */ - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: MigrationSourceHolder, - position: Int, - payloads: List?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: MigrationSourceHolder, + position: Int, + payloads: List? + ) { holder.bind(source, sourceEnabled) } @@ -53,7 +55,7 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): A } @Parcelize - data class ParcelableSI(val sourceId: Long, val sourceEnabled: Boolean): Parcelable + data class ParcelableSI(val sourceId: Long, val sourceEnabled: Boolean) : Parcelable fun asParcelable(): ParcelableSI { return ParcelableSI(source.id, sourceEnabled) @@ -69,4 +71,4 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): A ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt index 0f1432846a..02aadc0b49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/PreMigrationController.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.recyclerview.widget.LinearLayoutManager +import com.bluelinelabs.conductor.Router import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import eu.davidea.flexibleadapter.FlexibleAdapter @@ -17,13 +18,13 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController +import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets import eu.kanade.tachiyomi.util.view.marginBottom import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig -import kotlinx.android.synthetic.main.pre_migration_controller.fab -import kotlinx.android.synthetic.main.pre_migration_controller.recycler +import kotlinx.android.synthetic.main.pre_migration_controller.* import uy.kohesive.injekt.injectLazy class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), FlexibleAdapter @@ -37,7 +38,7 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F private var showingOptions = false - private var dialog:BottomSheetDialog? = null + private var dialog: BottomSheetDialog? = null override fun getTitle() = "Select target sources" @@ -47,6 +48,7 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForController() val ourAdapter = adapter ?: MigrationSourceAdapter( getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) }, @@ -62,7 +64,7 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F val fabBaseMarginBottom = fab?.marginBottom ?: 0 recycler.doOnApplyWindowInsets { v, insets, padding -> - fab?.updateLayoutParams { + fab?.updateLayoutParams { bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom } // offset the recycler by the fab's inset + some inset on top @@ -72,7 +74,7 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F fab.setOnClickListener { if (dialog?.isShowing != true) { - dialog = MigrationBottomSheetDialog(activity!!, R.style.SheetDialog, this) + dialog = MigrationBottomSheetDialog(activity!!, this) dialog?.show() val bottomSheet = dialog?.findViewById( com.google.android.material.R.id.design_bottom_sheet @@ -86,7 +88,7 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F } } - override fun startMigration(extraParam:String?) { + override fun startMigration(extraParam: String?) { val listOfSources = adapter?.items?.filter { it.sourceEnabled }?.joinToString("/") { it.source.id.toString() } @@ -98,7 +100,7 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F config.toList(), extraSearchParams = extraParam ) - ).withFadeTransaction()) + ).withFadeTransaction().tag(MigrationListController.TAG)) } override fun onSaveInstanceState(outState: Bundle) { @@ -112,7 +114,6 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F adapter?.onRestoreInstanceState(savedInstanceState) } - override fun onItemClick(view: View, position: Int): Boolean { adapter?.getItem(position)?.let { it.sourceEnabled = !it.sourceEnabled @@ -135,14 +136,14 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F .sortedBy { "(${it.lang}) ${it.name}" } sources = sources.filter { isEnabled(it.id.toString()) }.sortedBy { sourcesSaved.indexOf(it.id - .toString() ) + .toString()) } + sources.filterNot { isEnabled(it.id.toString()) } return sources } - fun isEnabled(id:String): Boolean { + fun isEnabled(id: String): Boolean { val sourcesSaved = prefs.migrationSources().getOrDefault() val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() return if (sourcesSaved.isEmpty()) id !in hiddenCatalogues @@ -152,10 +153,22 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController(bundle), F companion object { private const val MANGA_IDS_EXTRA = "manga_ids" + fun navigateToMigration(skipPre: Boolean, router: Router, mangaIds: List) { + router.pushController( + if (skipPre) { + MigrationListController.create( + MigrationProcedureConfig(mangaIds, null) + ) + } else { + create(mangaIds) + }.withFadeTransaction().tag(if (skipPre) MigrationListController.TAG else null) + ) + } + fun create(mangaIds: List): PreMigrationController { return PreMigrationController(Bundle().apply { putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray()) }) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt index a78823639d..cd0fa70336 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt @@ -10,10 +10,12 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlin.coroutines.CoroutineContext -class MigratingManga(private val db: DatabaseHelper, - private val sourceManager: SourceManager, - val mangaId: Long, - parentContext: CoroutineContext) { +class MigratingManga( + private val db: DatabaseHelper, + private val sourceManager: SourceManager, + val mangaId: Long, + parentContext: CoroutineContext +) { val searchResult = DeferredField() // @@ -21,12 +23,12 @@ class MigratingManga(private val db: DatabaseHelper, val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default - var migrationStatus:Int = MigrationStatus.RUNNUNG + var migrationStatus: Int = MigrationStatus.RUNNUNG @Volatile private var manga: Manga? = null suspend fun manga(): Manga? { - if(manga == null) manga = db.getManga(mangaId).executeAsBlocking() + if (manga == null) manga = db.getManga(mangaId).executeAsBlocking() return manga } @@ -46,4 +48,4 @@ class MigrationStatus { val MANGA_FOUND = 1 val MANGA_NOT_FOUND = 2 } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt index b53d14efe5..518543f014 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt @@ -14,6 +14,7 @@ import androidx.core.graphics.ColorUtils import androidx.recyclerview.widget.LinearLayoutManager import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga @@ -23,16 +24,23 @@ import eu.kanade.tachiyomi.smartsearch.SmartSearchEngine import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.main.BottomNavBarInterface +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.migration.MigrationMangaDialog import eu.kanade.tachiyomi.ui.migration.SearchController -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.system.await +import eu.kanade.tachiyomi.util.system.executeOnIO +import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.launchUI -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.chapters_controller.* +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController +import kotlinx.android.synthetic.main.migration_list_controller.* import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,13 +52,13 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import rx.schedulers.Schedulers +import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), - MigrationProcessAdapter.MigrationProcessInterface, - CoroutineScope { + MigrationProcessAdapter.MigrationProcessInterface, BottomNavBarInterface, CoroutineScope { init { setHasOptionsMenu(true) @@ -68,9 +76,10 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), private val smartSearchEngine = SmartSearchEngine(coroutineContext, config?.extraSearchParams) - private var migrationsJob: Job? = null + var migrationsJob: Job? = null + private set private var migratingManga: MutableList? = null - private var selectedPosition:Int? = null + private var selectedPosition: Int? = null private var manaulMigrations = 0 override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -78,13 +87,15 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), } override fun getTitle(): String? { - return resources?.getString(R.string.migration) + " (${adapter?.items?.count { it.manga - .migrationStatus != MigrationStatus.RUNNUNG }}/${adapter?.itemCount ?: 0})" + return resources?.getString(R.string.migration) + " (${adapter?.items?.count { + it.manga.migrationStatus != MigrationStatus.RUNNUNG + }}/${adapter?.itemCount ?: 0})" } override fun onViewCreated(view: View) { super.onViewCreated(view) + view.applyWindowInsetsForController() setTitle() val config = this.config ?: return @@ -103,23 +114,24 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), recycler.setHasFixedSize(true) recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - adapter?.updateDataSet(newMigratingManga.map { it.toModal() } ) + adapter?.updateDataSet(newMigratingManga.map { it.toModal() }) - if(migrationsJob == null) { + if (migrationsJob == null) { migrationsJob = launch { runMigrations(newMigratingManga) } } } - suspend fun runMigrations(mangas: List) { + private suspend fun runMigrations(mangas: List) { val useSourceWithMost = preferences.useSourceWithMost().getOrDefault() val sources = preferences.migrationSources().getOrDefault().split("/").mapNotNull { val value = it.toLongOrNull() ?: return - sourceManager.get(value) as? CatalogueSource } + sourceManager.get(value) as? CatalogueSource + } if (config == null) return - for(manga in mangas) { + for (manga in mangas) { if (migrationsJob?.isCancelled == true) { break } @@ -127,10 +139,10 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), if (manga.mangaId !in config.mangaIds) { continue } - if(!manga.searchResult.initialized && manga.migrationJob.isActive) { + if (!manga.searchResult.initialized && manga.migrationJob.isActive) { val mangaObj = manga.manga() - if(mangaObj == null) { + if (mangaObj == null) { manga.searchResult.initialize(null) continue } @@ -142,7 +154,7 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), val validSources = sources.filter { it.id != mangaSource.id } - if(useSourceWithMost) { + if (useSourceWithMost) { val sourceSemaphore = Semaphore(3) val processedSources = AtomicInteger() @@ -150,20 +162,33 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), async { sourceSemaphore.withPermit { try { - /* val searchResult = if (useSmartSearch) { - smartSearchEngine.smartSearch(source, mangaObj.title) - } else {*/ - val searchResult = smartSearchEngine - .normalSearch(source, mangaObj.originalTitle()) - - if(searchResult != null) { - val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id) - val chapters = source.fetchChapterList(localManga).toSingle().await( - Schedulers.io()) + /* val searchResult = if (useSmartSearch) { + smartSearchEngine.smartSearch(source, mangaObj.title) + } else {*/ + val searchResult = smartSearchEngine.normalSearch( + source, + mangaObj.title + ) + + if (searchResult != null) { + val localManga = + smartSearchEngine.networkToLocalManga( + searchResult, + source.id + ) + val chapters = + source.fetchChapterList(localManga).toSingle() + .await( + Schedulers.io() + ) try { - syncChaptersWithSource(db, chapters, localManga, source) - } - catch (e: Exception) { + syncChaptersWithSource( + db, + chapters, + localManga, + source + ) + } catch (e: Exception) { return@async null } manga.progress.send(validSources.size to processedSources.incrementAndGet()) @@ -171,10 +196,10 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), } else { null } - } catch(e: CancellationException) { + } catch (e: CancellationException) { // Ignore cancellations throw e - } catch(e: Exception) { + } catch (e: Exception) { null } } @@ -183,56 +208,65 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), } else { validSources.forEachIndexed { index, source -> val searchResult = try { - val searchResult = smartSearchEngine - .normalSearch(source, mangaObj.originalTitle()) + val searchResult = smartSearchEngine.normalSearch( + source, + mangaObj.title + ) if (searchResult != null) { - val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id) - val chapters = source.fetchChapterList(localManga).toSingle().await( - Schedulers.io()) + val localManga = smartSearchEngine.networkToLocalManga( + searchResult, + source.id + ) + val chapters = try { + source.fetchChapterList(localManga).toSingle() + .await(Schedulers.io()) + } catch (e: java.lang.Exception) { + Timber.e(e) + emptyList() + } ?: emptyList() withContext(Dispatchers.IO) { syncChaptersWithSource(db, chapters, localManga, source) } localManga } else null - } catch(e: CancellationException) { + } catch (e: CancellationException) { // Ignore cancellations throw e - } catch(e: Exception) { + } catch (e: Exception) { null } manga.progress.send(validSources.size to (index + 1)) - if(searchResult != null) return@async searchResult + if (searchResult != null) return@async searchResult } null } }.await() - } catch(e: CancellationException) { + } catch (e: CancellationException) { // Ignore canceled migrations continue } - if(result != null && result.thumbnail_url == null) { + if (result != null && result.thumbnail_url == null) { try { - val newManga = sourceManager.getOrStub(result.source) - .fetchMangaDetails(result) - .toSingle() - .await() + val newManga = + sourceManager.getOrStub(result.source).fetchMangaDetails(result) + .toSingle().await() result.copyFrom(newManga) db.insertManga(result).executeAsBlocking() - } catch(e: CancellationException) { + } catch (e: CancellationException) { // Ignore cancellations throw e - } catch(e: Exception) { + } catch (e: Exception) { } } - manga.migrationStatus = if (result == null) MigrationStatus.MANGA_NOT_FOUND else - MigrationStatus.MANGA_FOUND + manga.migrationStatus = + if (result == null) MigrationStatus.MANGA_NOT_FOUND else MigrationStatus.MANGA_FOUND adapter?.sourceFinished() manga.searchResult.initialize(result?.id) } @@ -264,8 +298,7 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), ids.removeAt(index) config.mangaIds = ids val index2 = migratingManga?.indexOf(item.manga) ?: return - if (index2 > -1) - migratingManga?.removeAt(index2) + if (index2 > -1) migratingManga?.removeAt(index2) } } @@ -296,7 +329,7 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), } } R.id.action_skip -> adapter?.removeManga(position) - R.id.action_migrate_now -> { + R.id.action_migrate_now -> { adapter?.migrateManga(position, false) manaulMigrations++ } @@ -345,7 +378,7 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), adapter?.notifyDataSetChanged() } else { migratingManga.manga.migrationStatus = MigrationStatus.MANGA_NOT_FOUND - activity?.toast(R.string.error_fetching_migration, Toast.LENGTH_LONG) + activity?.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG) adapter?.notifyDataSetChanged() } } @@ -354,22 +387,45 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), fun migrateMangas() { launchUI { adapter?.performMigrations(false) - router.popCurrentController() + navigateOut() } } fun copyMangas() { launchUI { adapter?.performMigrations(true) - router.popCurrentController() + navigateOut() } } + private fun navigateOut() { + if (migratingManga?.size == 1) { + launchUI { + val hasDetails = router.backstack.any { it.controller() is MangaDetailsController } + if (hasDetails) { + val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let { + db.getManga(it).executeOnIO() + } + if (manga != null) { + val newStack = router.backstack.filter { + it.controller() !is MangaDetailsController && + it.controller() !is MigrationListController && + it.controller() !is PreMigrationController + } + MangaDetailsController(manga).withFadeTransaction() + router.setBackstack(newStack, FadeChangeHandler()) + return@launchUI + } + } + router.popCurrentController() + } + } else router.popCurrentController() + } + override fun handleBack(): Boolean { activity?.let { MaterialDialog(it).show { - title(R.string.stop_migration) - positiveButton (R.string.action_stop) { + title(R.string.stop_migrating) + positiveButton(R.string.stop) { router.popCurrentController() migrationsJob?.cancel() } @@ -400,9 +456,13 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), resources!!, R.drawable.ic_done_white_24dp, null ) } - val translucentWhite = ColorUtils.setAlphaComponent(Color.WHITE, 127) - menuCopy.icon?.setTint(if (allMangasDone) Color.WHITE else translucentWhite) - menuMigrate?.icon?.setTint(if (allMangasDone) Color.WHITE else translucentWhite) + + menuCopy.icon.mutate() + menuMigrate.icon.mutate() + val tintColor = activity?.getResourceColor(R.attr.actionBarTintColor) ?: Color.WHITE + val translucentWhite = ColorUtils.setAlphaComponent(tintColor, 127) + menuCopy.icon?.setTint(if (allMangasDone) tintColor else translucentWhite) + menuMigrate?.icon?.setTint(if (allMangasDone) tintColor else translucentWhite) menuCopy.isEnabled = allMangasDone menuMigrate.isEnabled = allMangasDone } @@ -411,17 +471,43 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), val totalManga = adapter?.itemCount ?: 0 val mangaSkipped = adapter?.mangasSkipped() ?: 0 when (item.itemId) { - R.id.action_copy_manga -> MigrationMangaDialog(this, true, totalManga, mangaSkipped) - .showDialog(router) - R.id.action_migrate_manga -> MigrationMangaDialog(this, false, totalManga, mangaSkipped) - .showDialog(router) + R.id.action_copy_manga -> MigrationMangaDialog( + this, + true, + totalManga, + mangaSkipped + ).showDialog(router) + R.id.action_migrate_manga -> MigrationMangaDialog( + this, + false, + totalManga, + mangaSkipped + ).showDialog(router) else -> return super.onOptionsItemSelected(item) } return true } + override fun canChangeTabs(block: () -> Unit): Boolean { + if (migrationsJob?.isCancelled == false || adapter?.allMangasDone() == true) { + activity?.let { + MaterialDialog(it).show { + title(R.string.stop_migrating) + positiveButton(R.string.stop) { + block() + migrationsJob?.cancel() + } + negativeButton(android.R.string.cancel) + } + } + return false + } + return true + } + companion object { const val CONFIG_EXTRA = "config_extra" + const val TAG = "migration_list" fun create(config: MigrationProcedureConfig): MigrationListController { return MigrationListController(Bundle().apply { @@ -429,4 +515,4 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle), }) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt index 75e7e57fce..1c26143d25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt @@ -5,6 +5,6 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class MigrationProcedureConfig( - var mangaIds: List, - val extraSearchParams: String? -): Parcelable \ No newline at end of file + var mangaIds: List, + val extraSearchParams: String? +) : Parcelable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt index ce1521d9fe..1cdb3b3fb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.migration.manga.process -import android.content.Context import android.view.MenuItem import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -14,15 +13,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.withContext import uy.kohesive.injekt.injectLazy +import java.util.Date class MigrationProcessAdapter( val controller: MigrationListController ) : FlexibleAdapter(null, controller, true) { - private val db: DatabaseHelper by injectLazy() var items: List = emptyList() - val preferences:PreferencesHelper by injectLazy() + val preferences: PreferencesHelper by injectLazy() val menuItemListener: MigrationProcessInterface = controller @@ -46,7 +45,7 @@ class MigrationProcessAdapter( } fun allMangasDone() = (items.all { it.manga.migrationStatus != MigrationStatus - .RUNNUNG } && items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND }) + .RUNNUNG } && items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND }) fun mangasSkipped() = (items.count { it.manga.migrationStatus == MigrationStatus.MANGA_NOT_FOUND }) @@ -93,16 +92,18 @@ class MigrationProcessAdapter( sourceFinished() } - private fun migrateMangaInternal(prevManga: Manga, + private fun migrateMangaInternal( + prevManga: Manga, manga: Manga, - replace: Boolean) { + replace: Boolean + ) { if (controller.config == null) return val flags = preferences.migrateFlags().getOrDefault() // Update chapters read if (MigrationFlags.hasChapters(flags)) { val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() - val maxChapterRead = prevMangaChapters.filter { it.read } - .maxBy { it.chapter_number }?.chapter_number + val maxChapterRead = + prevMangaChapters.filter { it.read }.maxBy { it.chapter_number }?.chapter_number if (maxChapterRead != null) { val dbChapters = db.getChapters(manga).executeAsBlocking() for (chapter in dbChapters) { @@ -134,10 +135,10 @@ class MigrationProcessAdapter( db.updateMangaFavorite(prevManga).executeAsBlocking() } manga.favorite = true + if (replace) manga.date_added = prevManga.date_added + else manga.date_added = Date().time db.updateMangaFavorite(manga).executeAsBlocking() - - // SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title + db.updateMangaAdded(manga).executeAsBlocking() db.updateMangaTitle(manga).executeAsBlocking() - //} } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt index 835f34a560..31d6bffac6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt @@ -1,8 +1,10 @@ package eu.kanade.tachiyomi.ui.migration.manga.process import android.view.View -import android.widget.PopupMenu +import androidx.appcompat.widget.PopupMenu +import androidx.constraintlayout.widget.ConstraintLayout import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga @@ -11,15 +13,17 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.invisible -import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.visible -import kotlinx.android.synthetic.main.migration_manga_card.view.* +import eu.kanade.tachiyomi.widget.StateImageViewTarget +import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.migration_process_item.* +import kotlinx.android.synthetic.main.unread_download_badge.view.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import uy.kohesive.injekt.injectLazy @@ -32,8 +36,7 @@ class MigrationProcessHolder( private val db: DatabaseHelper by injectLazy() private val sourceManager: SourceManager by injectLazy() - private var item:MigrationProcessItem? = null - + private var item: MigrationProcessItem? = null init { // We need to post a Runnable to show the popup to make sure that the PopupMenu is @@ -62,10 +65,10 @@ class MigrationProcessHolder( migration_manga_card_to.resetManga() if (manga != null) { withContext(Dispatchers.Main) { - migration_manga_card_from.attachManga(manga, source) + migration_manga_card_from.attachManga(manga, source, false) migration_manga_card_from.setOnClickListener { adapter.controller.router.pushController( - MangaController( + MangaDetailsController( manga, true ).withFadeTransaction() ) @@ -94,16 +97,16 @@ class MigrationProcessHolder( return@withContext } if (searchResult != null && resultSource != null) { - migration_manga_card_to.attachManga(searchResult, resultSource) + migration_manga_card_to.attachManga(searchResult, resultSource, true) migration_manga_card_to.setOnClickListener { adapter.controller.router.pushController( - MangaController( + MangaDetailsController( searchResult, true ).withFadeTransaction() ) } } else { - migration_manga_card_to.loading_group.gone() + migration_manga_card_to.progress.gone() migration_manga_card_to.title.text = view.context.applicationContext.getString(R.string.no_alternatives_found) } @@ -116,32 +119,39 @@ class MigrationProcessHolder( } private fun View.resetManga() { - loading_group.visible() - thumbnail.setImageDrawable(null) + progress.visible() + cover_thumbnail.setImageDrawable(null) + compact_title.text = "" title.text = "" - manga_source_label.text = "" - manga_chapters.text = "" - manga_chapters.gone() - manga_last_chapter_label.text = "" + subtitle.text = "" + badge_view.setChapters(null) + (layoutParams as ConstraintLayout.LayoutParams).verticalBias = 0.5f + subtitle.text = "" migration_manga_card_to.setOnClickListener(null) } - private fun View.attachManga(manga: Manga, source: Source) { - loading_group.gone() - GlideApp.with(view.context.applicationContext) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(thumbnail) + private fun View.attachManga(manga: Manga, source: Source, isTo: Boolean) { + (layoutParams as ConstraintLayout.LayoutParams).verticalBias = 1f + progress.gone() + GlideApp.with(view.context.applicationContext).load(manga).apply { + diskCacheStrategy(DiskCacheStrategy.RESOURCE) + if (isTo) { + transition(DrawableTransitionOptions.withCrossFade()) + .into(StateImageViewTarget(cover_thumbnail, progress)) + } else + into(cover_thumbnail) + } - title.text = if (manga.currentTitle().isBlank()) { + compact_title.visible() + gradient.visible() + compact_title.text = if (manga.title.isBlank()) { view.context.getString(R.string.unknown) } else { - manga.currentTitle() + manga.title } gradient.visible() - manga_source_label.text = /*if (source.id == MERGED_SOURCE_ID) { + title.text = /*if (source.id == MERGED_SOURCE_ID) { MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { sourceManager.getOrStub(it.source).toString() }.distinct().joinToString() @@ -150,15 +160,14 @@ class MigrationProcessHolder( // } val mangaChapters = db.getChapters(manga).executeAsBlocking() - manga_chapters.visible() - manga_chapters.text = mangaChapters.size.toString() + badge_view.setChapters(mangaChapters.size) val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f if (latestChapter > 0f) { - manga_last_chapter_label.text = context.getString(R.string.latest_x, + subtitle.text = context.getString(R.string.latest_, DecimalFormat("#.#").format(latestChapter)) } else { - manga_last_chapter_label.text = context.getString(R.string.latest_x, + subtitle.text = context.getString(R.string.latest_, context.getString(R.string.unknown)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt index 70b0ebdcf7..df77a57cc5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt @@ -18,10 +18,12 @@ class MigrationProcessItem(val manga: MigratingManga) : return MigrationProcessHolder(view, adapter as MigrationProcessAdapter) } - override fun bindViewHolder(adapter: FlexibleAdapter>, + override fun bindViewHolder( + adapter: FlexibleAdapter>, holder: MigrationProcessHolder, position: Int, - payloads: MutableList?) { + payloads: MutableList? + ) { holder.bind(this) } @@ -36,5 +38,4 @@ class MigrationProcessItem(val manga: MigratingManga) : override fun hashCode(): Int { return manga.mangaId.toInt() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt index d7383723bb..df6dca5e05 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt @@ -15,8 +15,8 @@ import eu.kanade.tachiyomi.widget.OutlineSpan * Page indicator found at the bottom of the reader */ class PageIndicatorTextView( - context: Context, - attrs: AttributeSet? = null + context: Context, + attrs: AttributeSet? = null ) : AppCompatTextView(context, attrs) { init { @@ -32,7 +32,7 @@ class PageIndicatorTextView( // Also add a bit of spacing between each character, as the stroke overlaps them val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")).apply { // Apply text outline - setSpan(spanOutline, 1, length-1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan(spanOutline, 1, length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) for (i in 1..lastIndex step 2) { setSpan(ScaleXSpan(0.2f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 2f82a1a552..4c033b809f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -11,14 +11,19 @@ import android.graphics.Bitmap import android.graphics.Color import android.os.Build import android.os.Bundle -import android.view.* +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.SeekBar import androidx.appcompat.app.AppCompatDelegate -import androidx.biometric.BiometricManager import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.snackbar.Snackbar import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga @@ -27,9 +32,9 @@ import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity -import eu.kanade.tachiyomi.ui.main.BiometricActivity -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.* +import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst +import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error +import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters @@ -41,10 +46,12 @@ import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.GLUtil +import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.widget.SimpleAnimationListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener @@ -60,7 +67,7 @@ import rx.subscriptions.CompositeSubscription import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File -import java.util.* +import java.util.Locale import java.util.concurrent.TimeUnit import kotlin.math.abs @@ -122,6 +129,8 @@ class ReaderActivity : BaseRxActivity(), @Suppress("DEPRECATION") private var progressDialog: ProgressDialog? = null + private var snackbar: Snackbar? = null + companion object { @Suppress("unused") const val LEFT_TO_RIGHT = 1 @@ -143,20 +152,14 @@ class ReaderActivity : BaseRxActivity(), /** * Called when the activity is created. Initializes the presenter and configuration. */ - override fun onCreate(savedState: Bundle?) { - AppCompatDelegate.setDefaultNightMode( - when (preferences.theme()) { - 1 -> AppCompatDelegate.MODE_NIGHT_NO - 2, 3, 4 -> AppCompatDelegate.MODE_NIGHT_YES - else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } - ) + override fun onCreate(savedInstanceState: Bundle?) { + AppCompatDelegate.setDefaultNightMode(ThemeUtil.nightMode(preferences.theme())) setTheme(when (preferences.readerTheme().getOrDefault()) { 0 -> R.style.Theme_Base_Reader_Light 1 -> R.style.Theme_Base_Reader_Dark else -> R.style.Theme_Base_Reader }) - super.onCreate(savedState) + super.onCreate(savedInstanceState) setContentView(R.layout.reader_activity) setNotchCutoutMode() @@ -174,8 +177,8 @@ class ReaderActivity : BaseRxActivity(), else presenter.init(manga, chapterUrl) } - if (savedState != null) { - menuVisible = savedState.getBoolean(::menuVisible.name) + if (savedInstanceState != null) { + menuVisible = savedInstanceState.getBoolean(::menuVisible.name) } config = ReaderConfig() @@ -195,6 +198,8 @@ class ReaderActivity : BaseRxActivity(), bottomSheet = null progressDialog?.dismiss() progressDialog = null + snackbar?.dismiss() + snackbar = null } /** @@ -322,6 +327,7 @@ class ReaderActivity : BaseRxActivity(), menuVisible = visible if (visible) coroutine?.cancel() if (visible) { + snackbar?.dismiss() systemUi?.show() reader_menu.visibility = View.VISIBLE reader_menu_bottom.visibility = View.VISIBLE @@ -356,8 +362,7 @@ class ReaderActivity : BaseRxActivity(), val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom) reader_menu_bottom.startAnimation(bottomAnimation) } - } - else + } else reader_menu.visibility = View.GONE } menuStickyVisible = false @@ -369,13 +374,34 @@ class ReaderActivity : BaseRxActivity(), */ fun setManga(manga: Manga) { val prevViewer = viewer - val newViewer = when (presenter.getMangaViewer()) { + val noDefault = manga.viewer == -1 + val mangaViewer = presenter.getMangaViewer() + val newViewer = when (mangaViewer) { RIGHT_TO_LEFT -> R2LPagerViewer(this) VERTICAL -> VerticalPagerViewer(this) WEBTOON -> WebtoonViewer(this) else -> L2RPagerViewer(this) } + if (noDefault && presenter.manga?.viewer!! > 0) { + snackbar = reader_layout.snack( + getString( + R.string.reading_, getString( + when (mangaViewer) { + RIGHT_TO_LEFT -> R.string.right_to_left_viewer + VERTICAL -> R.string.vertical_viewer + WEBTOON -> R.string.webtoon_style + else -> R.string.left_to_right_viewer + } + ).toLowerCase(Locale.getDefault()) + ), 8000 + ) { + setAction(R.string.use_default) { + presenter.setMangaViewer(0) + } + } + } + // Destroy previous viewer if there was one if (prevViewer != null) { prevViewer.destroy() @@ -384,7 +410,7 @@ class ReaderActivity : BaseRxActivity(), viewer = newViewer viewer_container.addView(newViewer.getView()) - toolbar.title = manga.currentTitle() + toolbar.title = manga.title page_seekbar.isRTL = newViewer is R2LPagerViewer @@ -461,7 +487,7 @@ class ReaderActivity : BaseRxActivity(), */ @SuppressLint("SetTextI18n") fun onPageSelected(page: ReaderPage) { - presenter.onPageSelected(page) + val newChapter = presenter.onPageSelected(page) val pages = page.chapter.pages ?: return // Set bottom page number @@ -476,6 +502,10 @@ class ReaderActivity : BaseRxActivity(), left_page_text.text = "${pages.size}" } + if (newChapter && config?.showNewChapter == false) { + systemUi?.show() + } + // Set seekbar progress page_seekbar.max = pages.lastIndex page_seekbar.progress = page.index @@ -522,23 +552,6 @@ class ReaderActivity : BaseRxActivity(), presenter.shareImage(page) } - override fun onResume() { - super.onResume() - val useBiometrics = preferences.useBiometrics().getOrDefault() - if (useBiometrics && BiometricManager.from(this) - .canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { - if (!MainActivity.unlocked && (preferences.lockAfter().getOrDefault() <= 0 || Date() - .time >= - preferences.lastUnlock().getOrDefault() + 60 * 1000 * preferences.lockAfter().getOrDefault())) { - val intent = Intent(this, BiometricActivity::class.java) - startActivity(intent) - this.overridePendingTransition(0, 0) - } - } - else if (useBiometrics) - preferences.useBiometrics().set(false) - } - /** * Called from the presenter when a page is ready to be shared. It shows Android's default * sharing tool. @@ -551,7 +564,7 @@ class ReaderActivity : BaseRxActivity(), clipData = ClipData.newRawUri(null, stream) type = "image/*" } - startActivity(Intent.createChooser(intent, getString(R.string.action_share))) + startActivity(Intent.createChooser(intent, getString(R.string.share))) } /** @@ -592,8 +605,8 @@ class ReaderActivity : BaseRxActivity(), fun onSetAsCoverResult(result: ReaderPresenter.SetAsCoverResult) { toast(when (result) { Success -> R.string.cover_updated - AddToLibraryFirst -> R.string.notification_first_add_to_library - Error -> R.string.notification_cover_update_failed + AddToLibraryFirst -> R.string.must_be_in_library_to_edit + Error -> R.string.failed_to_update_cover }) } @@ -621,8 +634,7 @@ class ReaderActivity : BaseRxActivity(), }) toolbar.startAnimation(toolbarAnimation) } - } - else { + } else { if (menuStickyVisible && !menuVisible) { setMenuVisibility(false, animate = false) } @@ -638,16 +650,14 @@ class ReaderActivity : BaseRxActivity(), val currentOrientation = resources.configuration.orientation - if(currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { + if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { val params = window.attributes params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER } - } } - /** * Class that handles the user preferences of the reader. */ @@ -668,6 +678,8 @@ class ReaderActivity : BaseRxActivity(), */ private var customFilterColorSubscription: Subscription? = null + var showNewChapter = false + /** * Initializes the reader subscriptions. */ @@ -704,6 +716,9 @@ class ReaderActivity : BaseRxActivity(), subscriptions += preferences.colorFilterMode().asObservable() .subscribe { setColorFilter(preferences.colorFilter().getOrDefault()) } + + subscriptions += preferences.alwaysShowChapterTransition().asObservable() + .subscribe { showNewChapter = it } } /** @@ -853,7 +868,5 @@ class ReaderActivity : BaseRxActivity(), color_overlay.visibility = View.VISIBLE color_overlay.setFilterColor(value, preferences.colorFilterMode().getOrDefault()) } - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterSheet.kt index 57f88346dc..17ead0295e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterSheet.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader import android.graphics.Color +import android.os.Build import android.view.View import android.view.ViewGroup import android.widget.SeekBar @@ -11,11 +12,12 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.util.lang.plusAssign +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener import kotlinx.android.synthetic.main.reader_color_filter.* -import kotlinx.android.synthetic.main.reader_color_filter_sheet.brightness_overlay -import kotlinx.android.synthetic.main.reader_color_filter_sheet.color_overlay +import kotlinx.android.synthetic.main.reader_color_filter_sheet.* import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription @@ -26,7 +28,8 @@ import kotlin.math.abs /** * Color filter sheet to toggle custom filter and brightness overlay. */ -class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activity) { +class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog + (activity, R.style.BottomSheetDialogTheme) { private val preferences by injectLazy() @@ -51,6 +54,15 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ val view = activity.layoutInflater.inflate(R.layout.reader_color_filter_sheet, null) setContentView(view) + setEdgeToEdge(activity, view, 0) + window?.navigationBarColor = Color.TRANSPARENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + preferences.readerTheme().getOrDefault() == 0 && + activity.window.decorView.rootWindowInsets.systemWindowInsetRight == 0 && + activity.window.decorView.rootWindowInsets.systemWindowInsetLeft == 0) + window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + setBottomEdge(brightness_seekbar, activity) + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) // Initialize subscriptions. @@ -90,6 +102,12 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ preferences.customBrightness().set(isChecked) } + /*color_filter_mode.setOnClickListener { + val popupMenu = PopupMenu(context, color_filter_mode) + + popupMenu.menuInflater.inflate(R.menu.download_single, popupMenu.menu) + popupMenu.show() + }*/ color_filter_mode.onItemSelectedListener = IgnoreFirstSpinnerListener { position -> preferences.colorFilterMode().set(position) } @@ -181,7 +199,7 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ val green = getGreenFromColor(color) val blue = getBlueFromColor(color) - //Initialize values + // Initialize values with(view) { txt_color_filter_alpha_value.text = alpha.toString() @@ -324,5 +342,4 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ /** Integer mask of blue value **/ const val BLUE_MASK: Long = 0x000000FF } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt index 4c833bbd08..eb04c10133 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderColorFilterView.kt @@ -1,13 +1,16 @@ package eu.kanade.tachiyomi.ui.reader import android.content.Context -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode import android.util.AttributeSet import android.view.View class ReaderColorFilterView( - context: Context, - attrs: AttributeSet? = null + context: Context, + attrs: AttributeSet? = null ) : View(context, attrs) { private val colorFilterPaint: Paint = Paint() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt index da1acb6a0c..1bb05c92ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt @@ -1,21 +1,30 @@ package eu.kanade.tachiyomi.ui.reader +import android.graphics.Color +import android.os.Build import android.os.Bundle -import com.google.android.material.bottomsheet.BottomSheetDialog +import android.view.View import android.view.ViewGroup import com.afollestad.materialdialogs.MaterialDialog +import com.google.android.material.bottomsheet.BottomSheetDialog import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge import kotlinx.android.synthetic.main.reader_page_sheet.* +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** * Sheet to show when a page is long clicked. */ class ReaderPageSheet( - private val activity: ReaderActivity, - private val page: ReaderPage -) : BottomSheetDialog(activity) { + private val activity: ReaderActivity, + private val page: ReaderPage +) : BottomSheetDialog(activity, R.style.BottomSheetDialogTheme) { /** * View used on this sheet. @@ -24,6 +33,15 @@ class ReaderPageSheet( init { setContentView(view) + setEdgeToEdge(activity, view) + window?.navigationBarColor = Color.TRANSPARENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + Injekt.get().readerTheme().getOrDefault() == 0 && + activity.window.decorView.rootWindowInsets.systemWindowInsetRight == 0 && + activity.window.decorView.rootWindowInsets.systemWindowInsetLeft == 0) + window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + + setBottomEdge(save_layout, activity) set_as_cover_layout.setOnClickListener { setAsCover() } share_layout.setOnClickListener { share() } @@ -45,7 +63,7 @@ class ReaderPageSheet( if (page.status != Page.READY) return MaterialDialog(activity) - .title(R.string.confirm_set_image_as_cover) + .title(R.string.use_image_as_cover) .positiveButton(android.R.string.yes) { activity.setAsCover(page) dismiss() @@ -69,5 +87,4 @@ class ReaderPageSheet( activity.saveImage(page) dismiss() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 79f5877839..dc8064be58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -24,6 +24,10 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.ImageUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import rx.Completable import rx.Observable import rx.Subscription @@ -33,18 +37,18 @@ import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit /** * Presenter used by the activity to perform background operations. */ class ReaderPresenter( - private val db: DatabaseHelper = Injekt.get(), - private val sourceManager: SourceManager = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val coverCache: CoverCache = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get() + private val db: DatabaseHelper = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() ) : BasePresenter() { /** @@ -87,19 +91,19 @@ class ReaderPresenter( val dbChapters = db.getChapters(manga).executeAsBlocking() val selectedChapter = dbChapters.find { it.id == chapterId } - ?: error("Requested chapter of id $chapterId not found in chapter list") + ?: error("Requested chapter of id $chapterId not found in chapter list") val chaptersForReader = - if (preferences.skipRead()) { - val list = dbChapters.filter { !it.read }.toMutableList() - val find = list.find { it.id == chapterId } - if (find == null) { - list.add(selectedChapter) - } - list - } else { - dbChapters + if (preferences.skipRead()) { + val list = dbChapters.filter { !it.read }.toMutableList() + val find = list.find { it.id == chapterId } + if (find == null) { + list.add(selectedChapter) } + list + } else { + dbChapters + } when (manga.sorting) { Manga.SORTING_SOURCE -> ChapterLoadBySource().get(chaptersForReader) @@ -119,20 +123,6 @@ class ReaderPresenter( } } - /** - * Called when the presenter is destroyed. It saves the current progress and cleans up - * references on the currently active chapters. - */ - override fun onDestroy() { - super.onDestroy() - val currentChapters = viewerChaptersRelay.value - if (currentChapters != null) { - currentChapters.unref() - saveChapterProgress(currentChapters.currChapter) - saveChapterHistory(currentChapters.currChapter) - } - } - /** * Called when the presenter instance is being saved. It saves the currently active chapter * id and the last page read. @@ -152,6 +142,12 @@ class ReaderPresenter( */ fun onBackPressed() { deletePendingChapters() + val currentChapters = viewerChaptersRelay.value + if (currentChapters != null) { + currentChapters.unref() + saveChapterProgress(currentChapters.currChapter) + saveChapterHistory(currentChapters.currChapter) + } } /** @@ -178,12 +174,12 @@ class ReaderPresenter( if (!needsInit()) return db.getManga(mangaId).asRxObservable() - .first() - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { init(it, initialChapterId) } - .subscribeFirst({ _, _ -> - // Ignore onNext event - }, ReaderActivity::setInitialChapterError) + .first() + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { init(it, initialChapterId) } + .subscribeFirst({ _, _ -> + // Ignore onNext event + }, ReaderActivity::setInitialChapterError) } fun init(mangaId: Long, chapterUrl: String) { @@ -215,13 +211,13 @@ class ReaderPresenter( // Read chapterList from an io thread because it's retrieved lazily and would block main. activeChapterSubscription?.unsubscribe() activeChapterSubscription = Observable - .fromCallable { chapterList.first { chapterId == it.chapter.id } } - .flatMap { getLoadObservable(loader!!, it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ _, _ -> - // Ignore onNext event - }, ReaderActivity::setInitialChapterError) + .fromCallable { chapterList.first { chapterId == it.chapter.id } } + .flatMap { getLoadObservable(loader!!, it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ _, _ -> + // Ignore onNext event + }, ReaderActivity::setInitialChapterError) } /** @@ -232,27 +228,29 @@ class ReaderPresenter( * Callers must also handle the onError event. */ private fun getLoadObservable( - loader: ChapterLoader, - chapter: ReaderChapter + loader: ChapterLoader, + chapter: ReaderChapter ): Observable { return loader.loadChapter(chapter) - .andThen(Observable.fromCallable { - val chapterPos = chapterList.indexOf(chapter) - - ViewerChapters(chapter, - chapterList.getOrNull(chapterPos - 1), - chapterList.getOrNull(chapterPos + 1)) - }) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { newChapters -> - val oldChapters = viewerChaptersRelay.value - - // Add new references first to avoid unnecessary recycling - newChapters.ref() - oldChapters?.unref() - - viewerChaptersRelay.call(newChapters) - } + .andThen(Observable.fromCallable { + val chapterPos = chapterList.indexOf(chapter) + + ViewerChapters( + chapter, + chapterList.getOrNull(chapterPos - 1), + chapterList.getOrNull(chapterPos + 1) + ) + }) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { newChapters -> + val oldChapters = viewerChaptersRelay.value + + // Add new references first to avoid unnecessary recycling + newChapters.ref() + oldChapters?.unref() + + viewerChaptersRelay.call(newChapters) + } } /** @@ -266,10 +264,10 @@ class ReaderPresenter( activeChapterSubscription?.unsubscribe() activeChapterSubscription = getLoadObservable(loader, chapter) - .toCompletable() - .onErrorComplete() - .subscribe() - .also(::add) + .toCompletable() + .onErrorComplete() + .subscribe() + .also(::add) } /** @@ -284,13 +282,13 @@ class ReaderPresenter( activeChapterSubscription?.unsubscribe() activeChapterSubscription = getLoadObservable(loader, chapter) - .doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) } - .doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) } - .subscribeFirst({ view, _ -> - view.moveToPageIndex(0) - }, { _, _ -> - // Ignore onError event, viewers handle that state - }) + .doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) } + .doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) } + .subscribeFirst({ view, _ -> + view.moveToPageIndex(0) + }, { _, _ -> + // Ignore onError event, viewers handle that state + }) } /** @@ -307,12 +305,12 @@ class ReaderPresenter( val loader = loader ?: return loader.loadChapter(chapter) - .observeOn(AndroidSchedulers.mainThread()) - // Update current chapters whenever a chapter is preloaded - .doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) } - .onErrorComplete() - .subscribe() - .also(::add) + .observeOn(AndroidSchedulers.mainThread()) + // Update current chapters whenever a chapter is preloaded + .doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) } + .onErrorComplete() + .subscribe() + .also(::add) } /** @@ -320,13 +318,15 @@ class ReaderPresenter( * read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this * [page]'s chapter is different from the currently active. */ - fun onPageSelected(page: ReaderPage) { - val currentChapters = viewerChaptersRelay.value ?: return + fun onPageSelected(page: ReaderPage): Boolean { + val currentChapters = viewerChaptersRelay.value ?: return false val selectedChapter = page.chapter // Save last page read and mark as read if needed selectedChapter.chapter.last_page_read = page.index + selectedChapter.chapter.pages_left = + (selectedChapter.pages?.size ?: page.index) - page.index if (selectedChapter.pages?.lastIndex == page.index) { selectedChapter.chapter.read = true updateTrackChapterRead(selectedChapter) @@ -337,7 +337,9 @@ class ReaderPresenter( Timber.d("Setting ${selectedChapter.chapter.url} as active") onChapterChanged(currentChapters.currChapter, selectedChapter) loadNewChapter(selectedChapter) + return true } + return false } /** @@ -354,9 +356,9 @@ class ReaderPresenter( */ private fun saveChapterProgress(chapter: ReaderChapter) { db.updateChapterProgress(chapter.chapter).asRxCompletable() - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } /** @@ -364,10 +366,7 @@ class ReaderPresenter( */ private fun saveChapterHistory(chapter: ReaderChapter) { val history = History.create(chapter.chapter).apply { last_read = Date().time } - db.updateHistoryLastRead(history).asRxCompletable() - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + db.updateHistoryLastRead(history).executeAsBlocking() } /** @@ -405,6 +404,10 @@ class ReaderPresenter( */ fun getMangaViewer(): Int { val manga = manga ?: return preferences.defaultViewer() + if (manga.viewer == -1) { + manga.viewer = manga.defaultReaderType() + db.updateMangaViewer(manga).asRxObservable().subscribe() + } return if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer } @@ -417,18 +420,18 @@ class ReaderPresenter( db.updateMangaViewer(manga).executeAsBlocking() Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - val currChapters = viewerChaptersRelay.value - if (currChapters != null) { - // Save current page - val currChapter = currChapters.currChapter - currChapter.requestedPage = currChapter.chapter.last_page_read - - // Emit manga and chapters to the new viewer - view.setManga(manga) - view.setChapters(currChapters) - } - }) + .subscribeFirst({ view, _ -> + val currChapters = viewerChaptersRelay.value + if (currChapters != null) { + // Save current page + val currChapter = currChapters.currChapter + currChapter.requestedPage = currChapter.chapter.last_page_read + + // Emit manga and chapters to the new viewer + view.setManga(manga) + view.setChapters(currChapters) + } + }) } /** @@ -444,7 +447,7 @@ class ReaderPresenter( // Build destination file. val filename = DiskUtil.buildValidFilename( - "${manga.currentTitle()} - ${chapter.name}".take(225) + "${manga.title} - ${chapter.name}".take(225) ) + " - ${page.number}.${type.extension}" val destFile = File(directory, filename) @@ -469,23 +472,25 @@ class ReaderPresenter( notifier.onClear() // Pictures directory. - val destDir = File(Environment.getExternalStorageDirectory().absolutePath + + val destDir = File( + Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_PICTURES + - File.separator + "Tachiyomi") + File.separator + "Tachiyomi" + ) // Copy file in background. Observable.fromCallable { saveImage(page, destDir, manga) } - .doOnNext { file -> - DiskUtil.scanMedia(context, file) - notifier.onComplete(file) - } - .doOnError { notifier.onError(it.message) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) }, - { view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) } - ) + .doOnNext { file -> + DiskUtil.scanMedia(context, file) + notifier.onComplete(file) + } + .doOnError { notifier.onError(it.message) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) }, + { view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) } + ) } /** @@ -503,13 +508,13 @@ class ReaderPresenter( val destDir = File(context.cacheDir, "shared_image") Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file - .map { saveImage(page, destDir, manga) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, file -> view.onShareImageResult(file) }, - { _, _ -> /* Empty */ } - ) + .map { saveImage(page, destDir, manga) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, file -> view.onShareImageResult(file) }, + { _, _ -> /* Empty */ } + ) } /** @@ -521,29 +526,33 @@ class ReaderPresenter( val stream = page.stream ?: return Observable - .fromCallable { - if (manga.source == LocalSource.ID) { - val context = Injekt.get() - LocalSource.updateCover(context, manga, stream()) - R.string.cover_updated + .fromCallable { + if (manga.source == LocalSource.ID) { + val context = Injekt.get() + LocalSource.updateCover(context, manga, stream()) + R.string.cover_updated + SetAsCoverResult.Success + } else { + val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found") + if (manga.favorite) { + if (!manga.hasCustomCover()) { + manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}" + db.insertManga(manga).executeAsBlocking() + } + coverCache.copyToCache(manga.thumbnail_url!!, stream()) + MangaImpl.setLastCoverFetch(manga.id!!, Date().time) SetAsCoverResult.Success } else { - val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found") - if (manga.favorite) { - coverCache.copyToCache(thumbUrl, stream()) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - SetAsCoverResult.Success - } else { - SetAsCoverResult.AddToLibraryFirst - } + SetAsCoverResult.AddToLibraryFirst } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, result -> view.onSetAsCoverResult(result) }, - { view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) } - ) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, result -> view.onSetAsCoverResult(result) }, + { view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) } + ) } /** @@ -573,27 +582,24 @@ class ReaderPresenter( val trackManager = Injekt.get() - db.getTracks(manga).asRxSingle() - .flatMapCompletable { trackList -> - Completable.concat(trackList.map { track -> - val service = trackManager.getService(track.sync_id) - if (service != null && service.isLogged && chapterRead > track.last_chapter_read) { + // We wan't these to execute even if the presenter is destroyed so launch on GlobalScope + GlobalScope.launch { + withContext(Dispatchers.IO) { + val trackList = db.getTracks(manga).executeAsBlocking() + trackList.map { track -> + val service = trackManager.getService(track.sync_id) + if (service != null && service.isLogged && chapterRead > track.last_chapter_read) { + try { track.last_chapter_read = chapterRead - - // We wan't these to execute even if the presenter is destroyed and leaks - // for a while. The view can still be garbage collected. - Observable.defer { service.update(track) } - .map { db.insertTrack(track).executeAsBlocking() } - .toCompletable() - .onErrorComplete() - } else { - Completable.complete() + service.update(track) + db.insertTrack(track).executeAsBlocking() + } catch (e: Exception) { + Timber.e(e) } - }) + } } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + } + } } /** @@ -609,19 +615,19 @@ class ReaderPresenter( if (removeAfterReadSlots == -1) return Completable - .fromCallable { - // Position of the read chapter - val position = chapterList.indexOf(chapter) - - // Retrieve chapter to delete according to preference - val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots) - if (chapterToDelete != null) { - downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga) - } + .fromCallable { + // Position of the read chapter + val position = chapterList.indexOf(chapter) + + // Retrieve chapter to delete according to preference + val chapterToDelete = chapterList.getOrNull(position - removeAfterReadSlots) + if (chapterToDelete != null) { + downloadManager.enqueueDeleteChapters(listOf(chapterToDelete.chapter), manga) } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + } + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } /** @@ -630,9 +636,8 @@ class ReaderPresenter( */ private fun deletePendingChapters() { Completable.fromCallable { downloadManager.deletePendingChapters() } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + .onErrorComplete() + .subscribeOn(Schedulers.io()) + .subscribe() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSeekBar.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSeekBar.kt index 48aeb56d32..31576a4266 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSeekBar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSeekBar.kt @@ -4,16 +4,16 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Rect import android.os.Build -import androidx.appcompat.widget.AppCompatSeekBar import android.util.AttributeSet import android.view.MotionEvent +import androidx.appcompat.widget.AppCompatSeekBar /** * Seekbar to show current chapter progress. */ class ReaderSeekBar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null + context: Context, + attrs: AttributeSet? = null ) : AppCompatSeekBar(context, attrs) { /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt index 3b48be03a6..823eb47701 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt @@ -1,16 +1,26 @@ package eu.kanade.tachiyomi.ui.reader +import android.content.res.Configuration +import android.graphics.Color +import android.os.Build import android.os.Bundle -import com.google.android.material.bottomsheet.BottomSheetDialog -import androidx.core.widget.NestedScrollView +import android.view.View +import android.view.ViewGroup import android.widget.CompoundButton import android.widget.Spinner +import androidx.core.widget.NestedScrollView import com.f2prateek.rx.preferences.Preference +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import kotlinx.android.synthetic.main.reader_settings_sheet.* @@ -19,19 +29,46 @@ import uy.kohesive.injekt.injectLazy /** * Sheet to show reader and viewer preferences. */ -class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDialog(activity) { +class ReaderSettingsSheet(private val activity: ReaderActivity) : + BottomSheetDialog(activity, R.style.BottomSheetDialogTheme) { /** * Preferences helper. */ private val preferences by injectLazy() + private var sheetBehavior: BottomSheetBehavior<*> + + val scroll: NestedScrollView + init { // Use activity theme for this layout val view = activity.layoutInflater.inflate(R.layout.reader_settings_sheet, null) - val scroll = NestedScrollView(activity) + scroll = NestedScrollView(activity) scroll.addView(view) setContentView(scroll) + + sheetBehavior = BottomSheetBehavior.from(scroll.parent as ViewGroup) + setEdgeToEdge( + activity, scroll, if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) + 0 else -1 + ) + window?.navigationBarColor = Color.TRANSPARENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && preferences.readerTheme() + .getOrDefault() == 0 && activity.window.decorView.rootWindowInsets.systemWindowInsetRight == 0 && activity.window.decorView.rootWindowInsets.systemWindowInsetLeft == 0 + ) window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + val height = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + sheetBehavior.peekHeight = 200.dpToPx + height + + sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) {} + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.skipCollapsed = true + } + } + }) } /** @@ -46,6 +83,20 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia is PagerViewer -> initPagerPreferences() is WebtoonViewer -> initWebtoonPreferences() } + + setBottomEdge( + if (activity.viewer is PagerViewer) page_transitions else crop_borders_webtoon, activity + ) + + close_button.setOnClickListener { + dismiss() + } + } + + override fun onStart() { + super.onStart() + sheetBehavior.skipCollapsed = true + sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED } /** @@ -62,7 +113,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia show_page_number.bindToPreference(preferences.showPageNumber()) fullscreen.bindToPreference(preferences.fullscreen()) keepscreen.bindToPreference(preferences.keepScreenOn()) - long_tap.bindToPreference(preferences.readWithLongTap()) + always_show_chapter_transition.bindToPreference(preferences.alwaysShowChapterTransition()) } /** @@ -70,6 +121,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia */ private fun initPagerPreferences() { pager_prefs_group.visible() + webtoon_prefs_group.gone() scale_type.bindToPreference(preferences.imageScaleType(), 1) zoom_start.bindToPreference(preferences.zoomStart(), 1) crop_borders.bindToPreference(preferences.cropBorders()) @@ -81,6 +133,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia */ private fun initWebtoonPreferences() { webtoon_prefs_group.visible() + pager_prefs_group.gone() crop_borders_webtoon.bindToPreference(preferences.cropBordersWebtoon()) } @@ -95,15 +148,15 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia /** * Binds a spinner to an int preference with an optional offset for the value. */ - private fun Spinner.bindToPreference(pref: Preference, offset: Int = 0, shouldDismiss: - Boolean - = false) { + private fun Spinner.bindToPreference( + pref: Preference, + offset: Int = 0, + shouldDismiss: Boolean = false + ) { onItemSelectedListener = IgnoreFirstSpinnerListener { position -> pref.set(position + offset) - if (shouldDismiss) - dismiss() + if (shouldDismiss) dismiss() } setSelection(pref.getOrDefault() - offset, false) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt index aae8d8c1f7..4f35424802 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt @@ -66,11 +66,11 @@ class SaveImageNotifier(private val context: Context) { setContentIntent(NotificationHandler.openImagePendingActivity(context, file)) // Share action addAction(R.drawable.ic_share_grey_24dp, - context.getString(R.string.action_share), + context.getString(R.string.share), NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId)) // Delete action addAction(R.drawable.ic_delete_grey_24dp, - context.getString(R.string.action_delete), + context.getString(R.string.delete), NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId)) updateNotification() @@ -96,11 +96,10 @@ class SaveImageNotifier(private val context: Context) { fun onError(error: String?) { // Create notification with(notificationBuilder) { - setContentTitle(context.getString(R.string.download_notifier_title_error)) + setContentTitle(context.getString(R.string.download_error)) setContentText(error ?: context.getString(R.string.unknown_error)) setSmallIcon(android.R.drawable.ic_menu_report_image) } updateNotification() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 2bc91b0630..8038068c56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -16,9 +16,9 @@ import timber.log.Timber * Loader used to retrieve the [PageLoader] for a given chapter. */ class ChapterLoader( - private val downloadManager: DownloadManager, - private val manga: Manga, - private val source: Source + private val downloadManager: DownloadManager, + private val manga: Manga, + private val source: Source ) { /** @@ -26,7 +26,7 @@ class ChapterLoader( * completes if the chapter is already loaded. */ fun loadChapter(chapter: ReaderChapter): Completable { - if (chapter.state is ReaderChapter.State.Loaded) { + if (chapterIsReady(chapter)) { return Completable.complete() } @@ -61,6 +61,13 @@ class ChapterLoader( .doOnError { chapter.state = ReaderChapter.State.Error(it) } } + /** + * Checks [chapter] to be loaded based on present pages and loader in addition to state. + */ + private fun chapterIsReady(chapter: ReaderChapter): Boolean { + return chapter.state is ReaderChapter.State.Loaded && chapter.pageLoader != null + } + /** * Returns the page loader to use for this [chapter]. */ @@ -80,5 +87,4 @@ class ChapterLoader( else -> error("Loader not implemented") } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt index ebbc3d2d37..71671b6fc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt @@ -37,5 +37,4 @@ class DirectoryPageLoader(val file: File) : PageLoader() { override fun getPage(page: ReaderPage): Observable { return Observable.just(Page.READY) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index a21993a787..25eabe291a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -15,10 +15,10 @@ import uy.kohesive.injekt.injectLazy * Loader used to load a chapter from the downloaded chapters. */ class DownloadPageLoader( - private val chapter: ReaderChapter, - private val manga: Manga, - private val source: Source, - private val downloadManager: DownloadManager + private val chapter: ReaderChapter, + private val manga: Manga, + private val source: Source, + private val downloadManager: DownloadManager ) : PageLoader() { /** @@ -45,5 +45,4 @@ class DownloadPageLoader( override fun getPage(page: ReaderPage): Observable { return Observable.just(Page.READY) // TODO maybe check if file still exists? } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 43e9348c3b..06ef670d6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -50,5 +50,4 @@ class EpubPageLoader(file: File) : PageLoader() { Page.READY }) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index c34799d8e3..11cbf6b1b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.lang.plusAssign +import eu.kanade.tachiyomi.util.system.ImageUtil import rx.Completable import rx.Observable import rx.schedulers.Schedulers @@ -28,9 +28,9 @@ import kotlin.math.min * Loader used to load chapters from an online source. */ class HttpPageLoader( - private val chapter: ReaderChapter, - private val source: HttpSource, - private val chapterCache: ChapterCache = Injekt.get() + private val chapter: ReaderChapter, + private val source: HttpSource, + private val chapterCache: ChapterCache = Injekt.get() ) : PageLoader() { /** @@ -173,9 +173,9 @@ class HttpPageLoader( * Data class used to keep ordering of pages in order to maintain priority. */ private class PriorityPage( - val page: ReaderPage, - val priority: Int - ): Comparable { + val page: ReaderPage, + val priority: Int + ) : Comparable { companion object { private val idGenerator = AtomicInteger() @@ -187,7 +187,6 @@ class HttpPageLoader( val p = other.priority.compareTo(priority) return if (p != 0) p else identifier.compareTo(other.identifier) } - } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt index b0b872b6d7..de7e4e5410 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt @@ -42,5 +42,4 @@ abstract class PageLoader { * online source is used. */ open fun retryPage(page: ReaderPage) {} - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 51f25da0d9..2a4d31204c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -83,5 +83,4 @@ class RarPageLoader(file: File) : PageLoader() { } return pipeIn } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt index 892eceab04..41b6a3d787 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt @@ -6,10 +6,12 @@ sealed class ChapterTransition { abstract val to: ReaderChapter? class Prev( - override val from: ReaderChapter, override val to: ReaderChapter? + override val from: ReaderChapter, + override val to: ReaderChapter? ) : ChapterTransition() class Next( - override val from: ReaderChapter, override val to: ReaderChapter? + override val from: ReaderChapter, + override val to: ReaderChapter? ) : ChapterTransition() override fun equals(other: Any?): Boolean { @@ -29,5 +31,4 @@ sealed class ChapterTransition { override fun toString(): String { return "${javaClass.simpleName}(from=${from.chapter.url}, to=${to?.chapter?.url})" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt index 48e3540425..eab73a60a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt @@ -5,14 +5,13 @@ import eu.kanade.tachiyomi.source.model.Page import java.io.InputStream class ReaderPage( - index: Int, - url: String = "", - imageUrl: String? = null, - var stream: (() -> InputStream)? = null, - var bg: Drawable? = null, - var bgAlwaysWhite: Boolean? = null + index: Int, + url: String = "", + imageUrl: String? = null, + var stream: (() -> InputStream)? = null, + var bg: Drawable? = null, + var bgAlwaysWhite: Boolean? = null ) : Page(index, url, imageUrl, null) { lateinit var chapter: ReaderChapter - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt index 1e950c6c70..9d43048409 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ViewerChapters.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.ui.reader.model data class ViewerChapters( - val currChapter: ReaderChapter, - val prevChapter: ReaderChapter?, - val nextChapter: ReaderChapter? + val currChapter: ReaderChapter, + val prevChapter: ReaderChapter?, + val nextChapter: ReaderChapter? ) { fun ref() { @@ -17,5 +17,4 @@ data class ViewerChapters( prevChapter?.unref() nextChapter?.unref() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt index 7420db5188..223cb087f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt @@ -42,5 +42,4 @@ interface BaseViewer { * return true if the event was handled, false otherwise. */ fun handleGenericMotionEvent(event: MotionEvent): Boolean - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt index a96392b52f..52e1ab9bce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt @@ -12,8 +12,8 @@ import kotlin.math.abs * one conflicts with the quick scale feature. */ open class GestureDetectorWithLongTap( - context: Context, - listener: Listener + context: Context, + listener: Listener ) : GestureDetector(context, listener) { private val handler = Handler() @@ -71,5 +71,4 @@ open class GestureDetectorWithLongTap( open fun onLongTapConfirmed(ev: MotionEvent) { } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt index 704577bb2b..ed215bf812 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt @@ -24,9 +24,9 @@ import kotlin.math.min * user also approximately knows how much the operation will take. */ class ReaderProgressBar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { /** @@ -157,7 +157,7 @@ class ReaderProgressBar @JvmOverloads constructor( if (!animate) { visibility = View.GONE } else { - ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).apply { + ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).apply { interpolator = DecelerateInterpolator() duration = 1000 addListener(object : AnimatorListenerAdapter() { @@ -209,5 +209,4 @@ class ReaderProgressBar @JvmOverloads constructor( start() } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt index a256a5c6e0..b89bc9ee0c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt @@ -13,8 +13,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap * pager can also be declared to be vertical by creating it with [isHorizontal] to false. */ open class Pager( - context: Context, - isHorizontal: Boolean = true + context: Context, + isHorizontal: Boolean = true ) : DirectionalViewPager(context, isHorizontal) { /** @@ -104,5 +104,4 @@ open class Pager( fun setGestureDetectorEnabled(enabled: Boolean) { isGestureDetectorEnabled = enabled } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt index f184031a40..57cef1c8a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt @@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import android.annotation.SuppressLint import android.content.Context -import androidx.appcompat.widget.AppCompatButton import android.view.MotionEvent +import androidx.appcompat.widget.AppCompatButton /** * A button class to be used by child views of the pager viewer. All tap gestures are handled by @@ -21,5 +21,4 @@ class PagerButton(context: Context, viewer: PagerViewer) : AppCompatButton(conte false } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index 9bb95453a6..62a970ec76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -46,6 +46,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe var readerTheme = 0 private set + var alwaysShowChapterTransition = true + private set + init { preferences.readWithTapping() .register({ tappingEnabled = it }) @@ -76,6 +79,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe preferences.readerTheme() .register({ readerTheme = it }) + + preferences.alwaysShowChapterTransition() + .register({ alwaysShowChapterTransition = it }) } fun unsubscribe() { @@ -83,8 +89,8 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe } private fun Preference.register( - valueAssignment: (T) -> Unit, - onChanged: (T) -> Unit = {} + valueAssignment: (T) -> Unit, + onChanged: (T) -> Unit = {} ) { asObservable() .doOnNext(valueAssignment) @@ -115,5 +121,4 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe enum class ZoomType { Left, Center, Right } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 52a20b8c92..6c9f7c0cc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -35,9 +35,9 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType -import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.widget.ViewPagerAdapter @@ -56,8 +56,8 @@ import java.util.concurrent.TimeUnit */ @SuppressLint("ViewConstructor") class PagerPageHolder( - val viewer: PagerViewer, - val page: ReaderPage + val viewer: PagerViewer, + val page: ReaderPage ) : FrameLayout(viewer.activity), ViewPagerAdapter.PositionableView { /** @@ -271,8 +271,7 @@ class PagerPageHolder( page.bgAlwaysWhite = viewer.config.readerTheme == 2 } } - } - else { + } else { initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!)) } } else { @@ -345,7 +344,7 @@ class PagerPageHolder( subsamplingImageView = SubsamplingScaleImageView(context).apply { layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) setMaxTileSize(viewer.activity.maxBitmapSize) - setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) + setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) setDoubleTapZoomDuration(config.doubleTapAnimDuration) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setMinimumScaleType(config.imageScaleType) @@ -408,7 +407,7 @@ class PagerPageHolder( layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { gravity = Gravity.CENTER } - setText(R.string.action_retry) + setText(R.string.retry) setOnClickListener { page.chapter.pageLoader?.retryPage(page) } @@ -445,7 +444,7 @@ class PagerPageHolder( layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { setMargins(margins, margins, margins, margins) } - setText(R.string.action_retry) + setText(R.string.retry) setOnClickListener { page.chapter.pageLoader?.retryPage(page) } @@ -459,7 +458,7 @@ class PagerPageHolder( layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { setMargins(margins, margins, margins, margins) } - setText(R.string.action_open_in_browser) + setText(R.string.open_in_browser) setOnClickListener { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(imageUrl)) context.startActivity(intent) @@ -484,21 +483,21 @@ class PagerPageHolder( .transition(DrawableTransitionOptions.with(NoTransition.getFactory())) .listener(object : RequestListener { override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean ): Boolean { onImageDecodeError() return false } override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean ): Boolean { if (resource is GifDrawable) { resource.setLoopCount(GifDrawable.LOOP_INTRINSIC) @@ -509,5 +508,4 @@ class PagerPageHolder( }) .into(this) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 1798eb55a3..da50e94ace 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -27,8 +27,8 @@ import rx.android.schedulers.AndroidSchedulers */ @SuppressLint("ViewConstructor") class PagerTransitionHolder( - val viewer: PagerViewer, - val transition: ChapterTransition + val viewer: PagerViewer, + val transition: ChapterTransition ) : LinearLayout(viewer.activity), ViewPagerAdapter.PositionableView { /** @@ -46,8 +46,8 @@ class PagerTransitionHolder( * Text view used to display the text of the current and next/prev chapters. */ private var textView = TextView(context).apply { - //if (Build.VERSION.SDK_INT >= 23) - //setTextColor(context.getResourceColor(R.attr.)) + // if (Build.VERSION.SDK_INT >= 23) + // setTextColor(context.getResourceColor(R.attr.)) textSize = 17.5F wrapContent() } @@ -93,16 +93,16 @@ class PagerTransitionHolder( textView.text = if (nextChapter != null) { SpannableStringBuilder().apply { - append(context.getString(R.string.transition_finished)) + append(context.getString(R.string.finished)) setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${transition.from.chapter.name}\n\n") val currSize = length - append(context.getString(R.string.transition_next)) + append(context.getString(R.string.next)) setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${nextChapter.chapter.name}\n\n") } } else { - context.getString(R.string.transition_no_next) + context.getString(R.string.theres_no_next_chapter) } if (nextChapter != null) { @@ -118,16 +118,16 @@ class PagerTransitionHolder( textView.text = if (prevChapter != null) { SpannableStringBuilder().apply { - append(context.getString(R.string.transition_current)) + append(context.getString(R.string.current)) setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${transition.from.chapter.name}\n\n") val currSize = length - append(context.getString(R.string.transition_previous)) + append(context.getString(R.string.previous)) setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${prevChapter.chapter.name}\n\n") } } else { - context.getString(R.string.transition_no_previous) + context.getString(R.string.theres_no_previous_chapter) } if (prevChapter != null) { @@ -162,7 +162,7 @@ class PagerTransitionHolder( val textView = AppCompatTextView(context).apply { wrapContent() - setText(R.string.transition_pages_loading) + setText(R.string.loading_pages) } pagesContainer.addView(progress) @@ -182,12 +182,12 @@ class PagerTransitionHolder( private fun setError(error: Throwable) { val textView = AppCompatTextView(context).apply { wrapContent() - text = context.getString(R.string.transition_pages_error, error.message) + text = context.getString(R.string.failed_to_load_pages_, error.message) } val retryBtn = PagerButton(context, viewer).apply { wrapContent() - setText(R.string.action_retry) + setText(R.string.retry) setOnClickListener { val toChapter = transition.to if (toChapter != null) { @@ -206,5 +206,4 @@ class PagerTransitionHolder( private fun View.wrapContent() { layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index a8ab6bc00e..001f043624 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager -import androidx.viewpager.widget.ViewPager import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.view.ViewGroup.LayoutParams +import androidx.viewpager.widget.ViewPager import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition @@ -85,7 +85,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { else -> activity.toggleMenu() } } - pager.longTapListener = f@ { + pager.longTapListener = f@{ if (activity.menuVisible || config.longTapEnabled) { val item = adapter.items.getOrNull(pager.currentItem) if (item is ReaderPage) { @@ -144,8 +144,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { Timber.d("onReaderPageSelected: ${page.number}/${pages.size}") activity.onPageSelected(page) - if (page === pages.last()) { - Timber.d("Request preload next chapter because we're at the last page") + // Preload next chapter once we're within the last 3 pages of the current chapter + val inPreloadRange = pages.size - page.number < 3 + if (inPreloadRange) { + Timber.d("Request preload next chapter because we're at page ${page.number} of ${pages.size}") adapter.nextTransition?.to?.let { activity.requestPreloadChapter(it) } @@ -185,7 +187,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { */ private fun setChaptersInternal(chapters: ViewerChapters) { Timber.d("setChaptersInternal") - adapter.setChapters(chapters) + val forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull(pager + .currentItem) is ChapterTransition + adapter.setChapters(chapters, forceTransition) // Layout the pager once a chapter is being set if (pager.visibility == View.GONE) { @@ -323,5 +327,4 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { } return false } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt index 1e09183dd6..a23f4cac6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import android.view.View import android.view.ViewGroup import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition +import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.widget.ViewPagerAdapter @@ -27,7 +28,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { * next/previous chapter to allow seamless transitions and inverting the pages if the viewer * has R2L direction. */ - fun setChapters(chapters: ViewerChapters) { + fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) { val newItems = mutableListOf() // Add previous chapter pages and transition. @@ -39,7 +40,11 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { newItems.addAll(prevPages.takeLast(2)) } } - newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter)) + + // Skip transition page if the chapter is loaded & current page is not a transition page + if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { + newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter)) + } // Add current chapter. val currPages = chapters.currChapter.pages @@ -49,7 +54,13 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { // Add next chapter transition and pages. nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter) - .also { newItems.add(it) } + .also { + if (forceTransition || + chapters.nextChapter?.state !is ReaderChapter.State.Loaded) { + newItems.add(it) + } + } + if (chapters.nextChapter != null) { // Add at most two pages, because this chapter will be selected before the user can // swap more pages. @@ -100,5 +111,4 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { } return POSITION_NONE } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt index ddbf3c2ec2..d2897edf3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout +import androidx.recyclerview.widget.DiffUtil import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition +import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters @@ -24,7 +24,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R * Updates this adapter with the given [chapters]. It handles setting a few pages of the * next/previous chapter to allow seamless transitions. */ - fun setChapters(chapters: ViewerChapters) { + fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) { val newItems = mutableListOf() // Add previous chapter pages and transition. @@ -36,7 +36,11 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R newItems.addAll(prevPages.takeLast(2)) } } - newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter)) + + // Skip transition page if the chapter is loaded & current page is not a transition page + if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { + newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter)) + } // Add current chapter. val currPages = chapters.currChapter.pages @@ -45,7 +49,10 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R } // Add next chapter transition and pages. - newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)) + if (forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded) { + newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)) + } + if (chapters.nextChapter != null) { // Add at most two pages, because this chapter will be selected before the user can // swap more pages. @@ -121,8 +128,8 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R * Diff util callback used to dispatch delta updates instead of full dataset changes. */ private class Callback( - private val oldItems: List, - private val newItems: List + private val oldItems: List, + private val newItems: List ) : DiffUtil.Callback() { /** @@ -168,5 +175,4 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R */ const val TRANSITION_VIEW = 1 } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt index 293127cb38..10a01b1377 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt @@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder import rx.Subscription abstract class WebtoonBaseHolder( - view: View, - protected val viewer: WebtoonViewer + view: View, + protected val viewer: WebtoonViewer ) : BaseViewHolder(view) { /** @@ -42,5 +42,4 @@ abstract class WebtoonBaseHolder( protected fun View.wrapContent() { layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt index b610e3b330..f467047e4c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt @@ -34,6 +34,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { var doubleTapAnimDuration = 500 private set + var alwaysShowChapterTransition = true + private set + init { preferences.readWithTapping() .register({ tappingEnabled = it }) @@ -52,6 +55,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { preferences.readWithVolumeKeysInverted() .register({ volumeKeysInverted = it }) + + preferences.alwaysShowChapterTransition() + .register({ alwaysShowChapterTransition = it }) } fun unsubscribe() { @@ -59,8 +65,8 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { } private fun Preference.register( - valueAssignment: (T) -> Unit, - onChanged: (T) -> Unit = {} + valueAssignment: (T) -> Unit, + onChanged: (T) -> Unit = {} ) { asObservable() .doOnNext(valueAssignment) @@ -70,5 +76,4 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) { .subscribe() .addTo(subscriptions) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt index 955dc898e9..3f919569ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt @@ -68,13 +68,12 @@ class WebtoonFrame(context: Context) : FrameLayout(context) { } override fun onFling( - e1: MotionEvent?, - e2: MotionEvent?, - velocityX: Float, - velocityY: Float + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float ): Boolean { return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt index 34f9898a66..f6a00d2d49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt @@ -51,5 +51,4 @@ class WebtoonLayoutManager(activity: ReaderActivity) : LinearLayoutManager(activ return if (child == null) NO_POSITION else getPosition(child) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 3d171800c2..5187566b12 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -4,8 +4,6 @@ import android.annotation.SuppressLint import android.content.Intent import android.graphics.drawable.Drawable import android.net.Uri -import androidx.appcompat.widget.AppCompatButton -import androidx.appcompat.widget.AppCompatImageView import android.view.Gravity import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -14,6 +12,8 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatImageView import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.GlideException @@ -48,8 +48,8 @@ import java.util.concurrent.TimeUnit * @constructor creates a new webtoon holder. */ class WebtoonPageHolder( - private val frame: FrameLayout, - viewer: WebtoonViewer + private val frame: FrameLayout, + viewer: WebtoonViewer ) : WebtoonBaseHolder(frame, viewer) { /** @@ -328,7 +328,7 @@ class WebtoonPageHolder( val size = 48.dpToPx layoutParams = FrameLayout.LayoutParams(size, size).apply { gravity = Gravity.CENTER_HORIZONTAL - setMargins(0, parentHeight/4, 0, 0) + setMargins(0, parentHeight / 4, 0, 0) } } progressContainer.addView(progress) @@ -389,9 +389,9 @@ class WebtoonPageHolder( AppCompatButton(context).apply { layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL - setMargins(0, parentHeight/4, 0, 0) + setMargins(0, parentHeight / 4, 0, 0) } - setText(R.string.action_retry) + setText(R.string.retry) setOnClickListener { page?.let { it.chapter.pageLoader?.retryPage(it) } } @@ -411,7 +411,7 @@ class WebtoonPageHolder( val decodeLayout = LinearLayout(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, parentHeight).apply { - setMargins(0, parentHeight/6, 0, 0) + setMargins(0, parentHeight / 6, 0, 0) } gravity = Gravity.CENTER_HORIZONTAL orientation = LinearLayout.VERTICAL @@ -432,7 +432,7 @@ class WebtoonPageHolder( layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { setMargins(0, margins, 0, margins) } - setText(R.string.action_retry) + setText(R.string.retry) setOnClickListener { page?.let { it.chapter.pageLoader?.retryPage(it) } } @@ -446,7 +446,7 @@ class WebtoonPageHolder( layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { setMargins(0, margins, 0, margins) } - setText(R.string.action_open_in_browser) + setText(R.string.open_in_browser) setOnClickListener { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(imageUrl)) context.startActivity(intent) @@ -482,21 +482,21 @@ class WebtoonPageHolder( .transition(DrawableTransitionOptions.with(NoTransition.getFactory())) .listener(object : RequestListener { override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean ): Boolean { onImageDecodeError() return false } override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean ): Boolean { if (resource is GifDrawable) { resource.setLoopCount(GifDrawable.LOOP_INTRINSIC) @@ -507,5 +507,4 @@ class WebtoonPageHolder( }) .into(this) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt index 2744d91d6e..3b11be3be2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt @@ -9,7 +9,6 @@ import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.ViewConfiguration import android.view.animation.DecelerateInterpolator -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap import kotlin.math.abs @@ -18,9 +17,9 @@ import kotlin.math.abs * Implementation of a [RecyclerView] used by the webtoon reader. */ open class WebtoonRecyclerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 ) : androidx.recyclerview.widget.RecyclerView(context, attrs, defStyle) { private var isZooming = false @@ -77,12 +76,12 @@ open class WebtoonRecyclerView @JvmOverloads constructor( } private fun zoom( - fromRate: Float, - toRate: Float, - fromX: Float, - toX: Float, - fromY: Float, - toY: Float + fromRate: Float, + toRate: Float, + fromX: Float, + toX: Float, + fromY: Float, + toY: Float ) { isZooming = true val animatorSet = AnimatorSet() @@ -102,7 +101,6 @@ open class WebtoonRecyclerView @JvmOverloads constructor( animatorSet.start() animatorSet.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) { - } override fun onAnimationEnd(animation: Animator) { @@ -111,11 +109,9 @@ open class WebtoonRecyclerView @JvmOverloads constructor( } override fun onAnimationCancel(animation: Animator) { - } override fun onAnimationRepeat(animation: Animator) { - } }) } @@ -222,7 +218,6 @@ open class WebtoonRecyclerView @JvmOverloads constructor( performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) } } - } inner class Detector : GestureDetectorWithLongTap(context, listener) { @@ -310,7 +305,6 @@ open class WebtoonRecyclerView @JvmOverloads constructor( } return super.onTouchEvent(ev) } - } private companion object { @@ -318,5 +312,4 @@ open class WebtoonRecyclerView @JvmOverloads constructor( const val DEFAULT_RATE = 1f const val MAX_SCALE_RATE = 3f } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt index 692c0596ba..88f916161d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt @@ -10,12 +10,11 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView * webtoon viewer handles all the gestures. */ class WebtoonSubsamplingImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null + context: Context, + attrs: AttributeSet? = null ) : SubsamplingScaleImageView(context, attrs) { override fun onTouchEvent(event: MotionEvent): Boolean { return false } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index 9d033f6bfb..3604274903 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -24,8 +24,8 @@ import rx.android.schedulers.AndroidSchedulers * Holder of the webtoon viewer that contains a chapter transition. */ class WebtoonTransitionHolder( - val layout: LinearLayout, - viewer: WebtoonViewer + val layout: LinearLayout, + viewer: WebtoonViewer ) : WebtoonBaseHolder(layout, viewer) { /** @@ -93,16 +93,16 @@ class WebtoonTransitionHolder( textView.text = if (nextChapter != null) { SpannableStringBuilder().apply { - append(context.getString(R.string.transition_finished)) + append(context.getString(R.string.finished)) setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${transition.from.chapter.name}\n\n") val currSize = length - append(context.getString(R.string.transition_next)) + append(context.getString(R.string.next)) setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${nextChapter.chapter.name}\n\n") } } else { - context.getString(R.string.transition_no_next) + context.getString(R.string.theres_no_next_chapter) } if (nextChapter != null) { @@ -118,16 +118,16 @@ class WebtoonTransitionHolder( textView.text = if (prevChapter != null) { SpannableStringBuilder().apply { - append(context.getString(R.string.transition_current)) + append(context.getString(R.string.current)) setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${transition.from.chapter.name}\n\n") val currSize = length - append(context.getString(R.string.transition_previous)) + append(context.getString(R.string.previous)) setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) append("\n${prevChapter.chapter.name}\n\n") } } else { - context.getString(R.string.transition_no_previous) + context.getString(R.string.theres_no_previous_chapter) } if (prevChapter != null) { @@ -174,7 +174,7 @@ class WebtoonTransitionHolder( val textView = AppCompatTextView(context).apply { wrapContent() - setText(R.string.transition_pages_loading) + setText(R.string.loading_pages) } pagesContainer.addView(progress) @@ -194,12 +194,12 @@ class WebtoonTransitionHolder( private fun setError(error: Throwable, transition: ChapterTransition) { val textView = AppCompatTextView(context).apply { wrapContent() - text = context.getString(R.string.transition_pages_error, error.message) + text = context.getString(R.string.failed_to_load_pages_, error.message) } val retryBtn = AppCompatButton(context).apply { wrapContent() - setText(R.string.action_retry) + setText(R.string.retry) setOnClickListener { val toChapter = transition.to if (toChapter != null) { @@ -211,5 +211,4 @@ class WebtoonTransitionHolder( pagesContainer.addView(textView) pagesContainer.addView(retryBtn) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 2b05259b31..8e962c2d0c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -1,12 +1,12 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.WebtoonLayoutManager import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.WebtoonLayoutManager import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderPage @@ -98,7 +98,7 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer { else -> activity.toggleMenu() } } - recycler.longTapListener = f@ { event -> + recycler.longTapListener = f@{ event -> if (activity.menuVisible || config.longTapEnabled) { val child = recycler.findChildViewUnder(event.x, event.y) if (child != null) { @@ -142,9 +142,11 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer { Timber.d("onPageSelected: ${page.number}/${pages.size}") activity.onPageSelected(page) - if (page === pages.last()) { - Timber.d("Request preload next chapter because we're at the last page") - val transition = adapter.items.getOrNull(position + 1) as? ChapterTransition.Next + // Preload next chapter once we're within the last 3 pages of the current chapter + val inPreloadRange = pages.size - page.number < 3 + if (inPreloadRange) { + Timber.d("Request preload next chapter because we're at page ${page.number} of ${pages.size}") + val transition = adapter.items.getOrNull(pages.size + 1) as? ChapterTransition.Next if (transition?.to != null) { activity.requestPreloadChapter(transition.to) } @@ -172,7 +174,8 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer { */ override fun setChapters(chapters: ViewerChapters) { Timber.d("setChapters") - adapter.setChapters(chapters) + val forceTransition = config.alwaysShowChapterTransition || currentPage is ChapterTransition + adapter.setChapters(chapters, forceTransition) if (recycler.visibility == View.GONE) { Timber.d("Recycler first layout") @@ -252,5 +255,4 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer { override fun handleGenericMotionEvent(event: MotionEvent): Boolean { return false } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt deleted file mode 100644 index c5e8c3bf59..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent_updates - -import android.app.Dialog -import android.os.Bundle -import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class ConfirmDeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : ConfirmDeleteChaptersDialog.Listener { - - private var chaptersToDelete = emptyList() - - constructor(target: T, chaptersToDelete: List) : this() { - this.chaptersToDelete = chaptersToDelete - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialDialog(activity!!) - .message(R.string.confirm_delete_chapters) - .positiveButton(android.R.string.yes) { - (targetController as? Listener)?.deleteChapters(chaptersToDelete) - } - .negativeButton(android.R.string.no) - } - - interface Listener { - fun deleteChapters(chaptersToDelete: List) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DateItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DateItem.kt index e621eec2c3..78ab104281 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DateItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DateItem.kt @@ -11,7 +11,8 @@ import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R import java.util.Date -class DateItem(val date: Date) : AbstractHeaderItem() { +class DateItem(val date: Date, val addedString: Boolean = false) : AbstractHeaderItem() { override fun getLayoutRes(): Int { return R.layout.recent_chapters_section_item @@ -44,7 +45,9 @@ class DateItem(val date: Date) : AbstractHeaderItem() { val section_text: TextView = view.findViewById(R.id.section_text) fun bind(item: DateItem) { - section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) + val dateString = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) + section_text.text = + if (item.addedString) itemView.context.getString(R.string.added_, dateString) else dateString } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt index 9f40d31dd4..e572f72f90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt @@ -1,14 +1,13 @@ package eu.kanade.tachiyomi.ui.recent_updates import android.view.View -import android.widget.PopupMenu +import androidx.core.content.ContextCompat import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.view.setVectorCompat +import kotlinx.android.synthetic.main.download_button.* import kotlinx.android.synthetic.main.recent_chapters_item.* /** @@ -22,7 +21,7 @@ import kotlinx.android.synthetic.main.recent_chapters_item.* * @constructor creates a new recent chapter holder. */ class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) : - BaseFlexibleViewHolder(view, adapter) { + BaseChapterHolder(view, adapter) { /** * Color of read chapter @@ -40,10 +39,6 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha private var item: RecentChapterItem? = null init { - // We need to post a Runnable to show the popup to make sure that the PopupMenu is - // correctly positioned. The reason being that the view may change position before the - // PopupMenu is shown. - chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } manga_cover.setOnClickListener { adapter.coverClickListener.onCoverClick(adapterPosition) } @@ -61,10 +56,16 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha chapter_title.text = item.chapter.name // Set manga title - manga_title.text = item.manga.currentTitle() - - // Set the correct drawable for dropdown and update the tint to match theme. - chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) + manga_full_title.text = item.manga.title + + if (front_view.translationX == 0f) { + read.setImageDrawable( + ContextCompat.getDrawable( + read.context, if (item.read) R.drawable.ic_eye_off_24dp + else R.drawable.ic_eye_24dp + ) + ) + } // Set cover GlideApp.with(itemView.context).clear(manga_cover) @@ -79,74 +80,34 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // Check if chapter is read and set correct color if (item.chapter.read) { chapter_title.setTextColor(readColor) - manga_title.setTextColor(readColor) + manga_full_title.setTextColor(readColor) } else { chapter_title.setTextColor(unreadColor) - manga_title.setTextColor(unreadColor) + manga_full_title.setTextColor(unreadColor) } // Set chapter status - notifyStatus(item.status) + notifyStatus(item.status, item.progress) + resetFrontView() } - /** - * Updates chapter status in view. - * - * @param status download status - */ - fun notifyStatus(status: Int) = with(download_text) { - when (status) { - Download.QUEUE -> setText(R.string.chapter_queued) - Download.DOWNLOADING -> setText(R.string.chapter_downloading) - Download.DOWNLOADED -> setText(R.string.chapter_downloaded) - Download.ERROR -> setText(R.string.chapter_error) - else -> text = "" - } + private fun resetFrontView() { + if (front_view.translationX != 0f) itemView.post { adapter.notifyItemChanged(adapterPosition) } + } + + override fun getFrontView(): View { + return front_view + } + + override fun getRearRightView(): View { + return right_view } /** - * Show pop up menu + * Updates chapter status in view. * - * @param view view containing popup menu. + * @param status download status */ - private fun showPopupMenu(view: View) = item?.let { item -> - // Create a PopupMenu, giving it the clicked view for an anchor - val popup = PopupMenu(view.context, view) - - // Inflate our menu resource into the PopupMenu's Menu - popup.menuInflater.inflate(R.menu.chapter_recent, popup.menu) - - // Hide download and show delete if the chapter is downloaded and - if (item.isDownloaded) { - popup.menu.findItem(R.id.action_download).isVisible = false - popup.menu.findItem(R.id.action_delete).isVisible = true - } - - // Hide mark as unread when the chapter is unread - if (!item.chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) { - popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false - } - - // Hide mark as read when the chapter is read - if (item.chapter.read) { - popup.menu.findItem(R.id.action_mark_as_read).isVisible = false - } - - // Set a listener so we are notified if a menu item is clicked - popup.setOnMenuItemClickListener { menuItem -> - with(adapter.controller) { - when (menuItem.itemId) { - R.id.action_download -> downloadChapter(item) - R.id.action_delete -> deleteChapter(item) - R.id.action_mark_as_read -> markAsRead(listOf(item)) - R.id.action_mark_as_unread -> markAsUnread(listOf(item)) - } - } - - true - } - - // Finally show the PopupMenu - popup.show() - } + fun notifyStatus(status: Int, progress: Int) = + download_button.setDownloadStatus(status, progress) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt index ba0d0098e0..4f221d1ca7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt @@ -3,53 +3,34 @@ package eu.kanade.tachiyomi.ui.recent_updates import android.view.View import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterItem -class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem) : - AbstractSectionableItem(header) { - - private var _status: Int = 0 - - var status: Int - get() = download?.status ?: _status - set(value) { _status = value } - - @Transient var download: Download? = null - - val isDownloaded: Boolean - get() = status == Download.DOWNLOADED +class RecentChapterItem(chapter: Chapter, val manga: Manga, header: DateItem) : + BaseChapterItem(chapter, header) { override fun getLayoutRes(): Int { return R.layout.recent_chapters_item } override fun createViewHolder(view: View, adapter: FlexibleAdapter>): RecentChapterHolder { - return RecentChapterHolder(view , adapter as RecentChaptersAdapter) + return RecentChapterHolder(view, adapter as RecentChaptersAdapter) } - override fun bindViewHolder(adapter: FlexibleAdapter>, - holder: RecentChapterHolder, - position: Int, - payloads: MutableList?) { - + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: RecentChapterHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(this) } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is RecentChapterItem) { - return chapter.id!! == other.chapter.id!! - } - return false - } - - override fun hashCode(): Int { - return chapter.id!!.hashCode() + fun filter(text: String): Boolean { + return chapter.name.contains(text, false) || + manga.title.contains(text, false) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt index 233c90fcea..6d72497641 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt @@ -1,19 +1,45 @@ package eu.kanade.tachiyomi.ui.recent_updates -import eu.davidea.flexibleadapter.FlexibleAdapter +import androidx.recyclerview.widget.ItemTouchHelper import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter class RecentChaptersAdapter(val controller: RecentChaptersController) : - FlexibleAdapter>(null, controller, true) { + BaseChapterAdapter>(controller) { val coverClickListener: OnCoverClickListener = controller + var recents = emptyList() + private var isAnimating = false init { setDisplayHeadersAtStartUp(true) - setStickyHeaders(true) + // setStickyHeaders(true) + } + + fun setItems(recents: List) { + this.recents = recents + performFilter() + } + + fun performFilter() { + val s = getFilter(String::class.java) + if (s.isNullOrBlank()) { + updateDataSet(recents, isAnimating) + } else { + updateDataSet(recents.filter { it.filter(s) }, isAnimating) + } + isAnimating = false } interface OnCoverClickListener { fun onCoverClick(position: Int) } -} \ No newline at end of file + + override fun onItemSwiped(position: Int, direction: Int) { + super.onItemSwiped(position, direction) + isAnimating = true + when (direction) { + ItemTouchHelper.LEFT -> controller.toggleMarkAsRead(position) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt index 7cc7be9b4e..cd908985f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt @@ -1,32 +1,35 @@ package eu.kanade.tachiyomi.ui.recent_updates +import android.app.Activity +import android.os.Bundle import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager -import com.jakewharton.rxbinding.support.v4.widget.refreshes -import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.system.notificationManager -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.scrollViewWith +import eu.kanade.tachiyomi.util.view.setStyle import eu.kanade.tachiyomi.util.view.snack +import kotlinx.android.synthetic.main.download_bottom_sheet.* import kotlinx.android.synthetic.main.recent_chapters_controller.* +import kotlinx.android.synthetic.main.recent_chapters_controller.empty_view import timber.log.Timber /** @@ -34,19 +37,10 @@ import timber.log.Timber * Uses [R.layout.recent_chapters_controller]. * UI related actions should be called from here. */ -class RecentChaptersController : NucleusController(), - NoToolbarElevationController, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.OnUpdateListener, - ConfirmDeleteChaptersDialog.Listener, - RecentChaptersAdapter.OnCoverClickListener { - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null +class RecentChaptersController(bundle: Bundle? = null) : BaseController(bundle), + FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnUpdateListener, + FlexibleAdapter.OnItemMoveListener, + RecentChaptersAdapter.OnCoverClickListener, BaseChapterAdapter.DownloadInterface { /** * Adapter containing the recent chapters. @@ -54,12 +48,12 @@ class RecentChaptersController : NucleusController(), var adapter: RecentChaptersAdapter? = null private set - override fun getTitle(): String? { - return resources?.getString(R.string.label_recent_updates) - } + private var presenter = RecentChaptersPresenter(this) + private var snack: Snackbar? = null + private var lastChapterId: Long? = null - override fun createPresenter(): RecentChaptersPresenter { - return RecentChaptersPresenter() + override fun getTitle(): String? { + return resources?.getString(R.string.recent_updates) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -72,6 +66,8 @@ class RecentChaptersController : NucleusController(), */ override fun onViewCreated(view: View) { super.onViewCreated(view) + // view.applyWindowInsetsForController() + view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS) // Init RecyclerView and adapter val layoutManager = LinearLayoutManager(view.context) @@ -81,39 +77,48 @@ class RecentChaptersController : NucleusController(), adapter = RecentChaptersAdapter(this@RecentChaptersController) recycler.adapter = adapter - recycler.scrollStateChanges().subscribeUntilDestroy { - // Disable swipe refresh when view is not at the top - val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos <= 0 - } - + adapter?.isSwipeEnabled = true + adapter?.itemTouchHelperCallback?.setSwipeFlags( + ItemTouchHelper.LEFT + ) + if (presenter.chapters.isNotEmpty()) adapter?.updateDataSet(presenter.chapters.toList()) + swipe_refresh.setStyle() swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt()) - swipe_refresh.refreshes().subscribeUntilDestroy { - if (!LibraryUpdateService.isRunning(view.context)) { + swipe_refresh.setOnRefreshListener { + if (!LibraryUpdateService.isRunning()) { LibraryUpdateService.start(view.context) - view.snack(R.string.updating_library) + snack = view.snack(R.string.updating_library) } // It can be a very long operation, so we disable swipe refresh and show a snackbar. swipe_refresh.isRefreshing = false } - recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + + scrollViewWith(recycler, swipeRefreshLayout = swipe_refresh, padBottom = true) + + presenter.onCreate() + } + + override fun onDestroy() { + super.onDestroy() + presenter.onDestroy() } override fun onDestroyView(view: View) { adapter = null - actionMode = null + snack = null super.onDestroyView(view) } - /** - * Returns selected chapters - * @return list of selected chapters - */ - fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + if (view != null) { + refresh() + dl_bottom_sheet?.update() + } } + fun refresh() = presenter.getUpdates() + /** * Called when item in list is clicked * @param position position of clicked item @@ -123,34 +128,8 @@ class RecentChaptersController : NucleusController(), // Get item from position val item = adapter.getItem(position) as? RecentChapterItem ?: return false - if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item) - return false - } - } - - /** - * Called when item in list is long clicked - * @param position position of clicked item - */ - override fun onItemLongClick(position: Int) { - if (actionMode == null) - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - - toggleSelection(position) - } - - /** - * Called to toggle selection - * @param position position of selected item - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - adapter.toggleSelection(position) - actionMode?.invalidate() + openChapter(item) + return false } /** @@ -163,38 +142,42 @@ class RecentChaptersController : NucleusController(), startActivity(intent) } - /** - * Download selected items - * @param chapters list of selected [RecentChapter]s - */ - fun downloadChapters(chapters: List) { - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - } - /** * Populate adapter with chapters * @param chapters list of [Any] */ - fun onNextRecentChapters(chapters: List>) { - destroyActionModeIfNeeded() - adapter?.updateDataSet(chapters) + fun onNextRecentChapters(chapters: List) { + adapter?.setItems(chapters) + } + + fun updateChapterDownload(download: Download) { + if (view == null) return + val id = download.chapter.id ?: return + val holder = recycler.findViewHolderForItemId(id) as? RecentChapterHolder ?: return + holder.notifyStatus(download.status, download.progress) } override fun onUpdateEmptyView(size: Int) { if (size > 0) { empty_view?.hide() } else { - empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent) + empty_view?.show(R.drawable.ic_update_black_128dp, R.string.no_recent_chapters) } } + override fun onItemMove(fromPosition: Int, toPosition: Int) { } + override fun shouldMoveItem(fromPosition: Int, toPosition: Int) = true + + override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + swipe_refresh.isEnabled = actionState != ItemTouchHelper.ACTION_STATE_SWIPE + } + /** * Update download status of chapter * @param download [Download] object containing download progress. */ fun onChapterStatusChange(download: Download) { - getHolder(download)?.notifyStatus(download.status) + getHolder(download)?.notifyStatus(download.status, download.progress) } /** @@ -207,59 +190,63 @@ class RecentChaptersController : NucleusController(), /** * Mark chapter as read - * @param chapters list of chapters + * @param position position of chapter item */ - fun markAsRead(chapters: List) { - presenter.markChapterRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) + fun toggleMarkAsRead(position: Int) { + val item = adapter?.getItem(position) as? RecentChapterItem ?: return + val chapter = item.chapter + val lastRead = chapter.last_page_read + val pagesLeft = chapter.pages_left + val read = item.chapter.read + lastChapterId = chapter.id + presenter.markChapterRead(item, !read) + if (!read) { + snack = view?.snack(R.string.marked_as_read, Snackbar.LENGTH_INDEFINITE) { + var undoing = false + setAction(R.string.undo) { + presenter.markChapterRead(item, read, lastRead, pagesLeft) + undoing = true + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!undoing && presenter.preferences.removeAfterMarkedAsRead()) { + lastChapterId = chapter.id + presenter.deleteChapter(chapter, item.manga) + } + } + }) + } + (activity as? MainActivity)?.setUndoSnackBar(snack) } + // presenter.markChapterRead(item, !item.chapter.read) } - override fun deleteChapters(chaptersToDelete: List) { - destroyActionModeIfNeeded() - presenter.deleteChapters(chaptersToDelete) - } - - /** - * Destory [ActionMode] if it's shown - */ - private fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - /** - * Mark chapter as unread - * @param chapters list of selected [RecentChapter] - */ - fun markAsUnread(chapters: List) { - presenter.markChapterRead(chapters, false) - } - - /** - * Start downloading chapter - * @param chapter selected chapter with manga - */ - fun downloadChapter(chapter: RecentChapterItem) { - presenter.downloadChapters(listOf(chapter)) + override fun downloadChapter(position: Int) { + val view = view ?: return + val item = adapter?.getItem(position) as? RecentChapterItem ?: return + val chapter = item.chapter + val manga = item.manga + if (item.status != Download.NOT_DOWNLOADED && item.status != Download.ERROR) { + presenter.deleteChapter(chapter, manga) + } else { + if (item.status == Download.ERROR) DownloadService.start(view.context) + else presenter.downloadChapters(listOf(item)) + } } - /** - * Start deleting chapter - * @param chapter selected chapter with manga - */ - fun deleteChapter(chapter: RecentChapterItem) { - presenter.deleteChapters(listOf(chapter)) + override fun startDownloadNow(position: Int) { + val chapter = (adapter?.getItem(position) as? RecentChapterItem)?.chapter ?: return + presenter.startDownloadChapterNow(chapter) } override fun onCoverClick(position: Int) { val chapterClicked = adapter?.getItem(position) as? RecentChapterItem ?: return openManga(chapterClicked) - } fun openManga(chapter: RecentChapterItem) { - router.pushController(MangaController(chapter.manga).withFadeTransaction()) + router.pushController(MangaDetailsController(chapter.manga).withFadeTransaction()) } /** @@ -276,54 +263,4 @@ class RecentChaptersController : NucleusController(), fun onChaptersDeletedError(error: Throwable) { Timber.e(error) } - - /** - * Called when ActionMode created. - * @param mode the ActionMode object - * @param menu menu object of ActionMode - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = resources?.getString(R.string.label_selected, count) - } - return false - } - - /** - * Called when ActionMode item clicked - * @param mode the ActionMode object - * @param item item from ActionMode. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters()) - .showDialog(router) - else -> return false - } - return true - } - - /** - * Called when ActionMode destroyed - * @param mode the ActionMode object - */ - override fun onDestroyActionMode(mode: ActionMode?) { - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt index b4e99693c3..9309054b30 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt @@ -1,17 +1,24 @@ package eu.kanade.tachiyomi.ui.recent_updates -import android.os.Bundle import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.download.model.DownloadQueue +import eu.kanade.tachiyomi.data.library.LibraryServiceListener +import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber +import eu.kanade.tachiyomi.util.system.executeOnIO +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Calendar @@ -19,67 +26,64 @@ import java.util.Date import java.util.TreeMap class RecentChaptersPresenter( - val preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val sourceManager: SourceManager = Injekt.get() -) : BasePresenter() { + private val controller: RecentChaptersController, + val preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get() +) : DownloadQueue.DownloadListener, LibraryServiceListener { /** * List containing chapter and manga information */ - private var chapters: List = emptyList() + var chapters: List = emptyList() - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + private var scope = CoroutineScope(Job() + Dispatchers.Default) - getRecentChaptersObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(RecentChaptersController::onNextRecentChapters) + fun onCreate() { + downloadManager.addListener(this) + LibraryUpdateService.setListener(this) + getUpdates() + } - getChapterStatusObservable() - .subscribeLatestCache(RecentChaptersController::onChapterStatusChange) { - _, error -> Timber.e(error) - } + fun getUpdates() { + scope.launch { + val cal = Calendar.getInstance().apply { + time = Date() + add(Calendar.MONTH, -1) + } + val mangaChapters = db.getRecentChapters(cal.time).executeOnIO() + val map = TreeMap> { d1, d2 -> d2.compareTo(d1) } + val byDay = mangaChapters.groupByTo(map, { getMapKey(it.chapter.date_fetch) }) + val items = byDay.flatMap { + val dateItem = DateItem(it.key) + it.value.map { mc -> + RecentChapterItem(mc.chapter, mc.manga, dateItem) } + } + setDownloadedChapters(items) + chapters = items + withContext(Dispatchers.Main) { controller.onNextRecentChapters(chapters) } + } } - /** - * Get observable containing recent chapters and date - * - * @return observable containing recent chapters and date - */ - fun getRecentChaptersObservable(): Observable> { - // Set date limit for recent chapters - val cal = Calendar.getInstance().apply { - time = Date() - add(Calendar.MONTH, -1) + fun onDestroy() { + downloadManager.removeListener(this) + LibraryUpdateService.removeListener(this) + } + + fun cancelScope() { + scope.cancel() + } + + override fun updateDownload(download: Download) { + chapters.find { it.chapter.id == download.chapter.id }?.download = download + scope.launch(Dispatchers.Main) { + controller.updateChapterDownload(download) } + } - return db.getRecentChapters(cal.time).asRxObservable() - // Convert to a list of recent chapters. - .map { mangaChapters -> - val map = TreeMap> { d1, d2 -> d2.compareTo(d1) } - val byDay = mangaChapters - .groupByTo(map, { getMapKey(it.chapter.date_fetch) }) - byDay.flatMap { - val dateItem = DateItem(it.key) - it.value.map { RecentChapterItem(it.chapter, it.manga, dateItem) } - } - } - .doOnNext { - it.forEach { item -> - // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == item.chapter.id } - - // If there's an active download, assign it, otherwise ask the manager if - // the chapter is downloaded and assign it to the status. - if (download != null) { - item.download = download - } - } - setDownloadedChapters(it) - chapters = it - } + override fun onUpdateManga(manga: LibraryManga) { + getUpdates() } /** @@ -98,44 +102,18 @@ class RecentChaptersPresenter( return cal.time } - /** - * Returns observable containing chapter status. - * - * @return download object containing download progress. - */ - private fun getChapterStatusObservable(): Observable { - return downloadManager.queue.getStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { download -> onDownloadStatusChange(download) } - } - /** * Finds and assigns the list of downloaded chapters. * - * @param items the list of chapter from the database. + * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(items: List) { - for (item in items) { - val manga = item.manga - val chapter = item.chapter - - if (downloadManager.isChapterDownloaded(chapter, manga)) { + private fun setDownloadedChapters(chapters: List) { + for (item in chapters) { + if (downloadManager.isChapterDownloaded(item.chapter, item.manga)) { item.status = Download.DOWNLOADED - } - } - } - - /** - * Update status of chapters. - * - * @param download download object containing progress. - */ - private fun onDownloadStatusChange(download: Download) { - // Assign the download to the model object. - if (download.status == Download.QUEUE) { - val chapter = chapters.find { it.chapter.id == download.chapter.id } - if (chapter != null && chapter.download == null) { - chapter.download = download + } else if (downloadManager.hasQueue()) { + item.status = downloadManager.queue.find { it.chapter.id == item.chapter.id } + ?.status ?: 0 } } } @@ -146,33 +124,44 @@ class RecentChaptersPresenter( * @param items list of selected chapters * @param read read status */ - fun markChapterRead(items: List, read: Boolean) { - val chapters = items.map { it.chapter } - chapters.forEach { - it.read = read + fun markChapterRead( + item: RecentChapterItem, + read: Boolean, + lastRead: Int? = null, + pagesLeft: Int? = null + ) { + item.chapter.apply { + this.read = read if (!read) { - it.last_page_read = 0 + last_page_read = lastRead ?: 0 + pages_left = pagesLeft ?: 0 } } + db.updateChapterProgress(item.chapter).executeAsBlocking() + controller.onNextRecentChapters(this.chapters) + } - Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() } - .subscribeOn(Schedulers.io()) - .subscribe() + fun startDownloadChapterNow(chapter: Chapter) { + downloadManager.startDownloadNow(chapter) } /** - * Delete selected chapters - * - * @param chapters list of chapters + * Deletes the given list of chapter. + * @param chapter the chapter to delete. */ - fun deleteChapters(chapters: List) { - Observable.just(chapters) - .doOnNext { deleteChaptersInternal(it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onChaptersDeleted() - }, RecentChaptersController::onChaptersDeletedError) + fun deleteChapter(chapter: Chapter, manga: Manga, update: Boolean = true) { + val source = Injekt.get().getOrStub(manga.source) + downloadManager.deleteChapters(listOf(chapter), manga, source) + + if (update) { + val item = chapters.find { it.chapter.id == chapter.id } ?: return + item.apply { + status = Download.NOT_DOWNLOADED + download = null + } + + controller.onNextRecentChapters(chapters) + } } /** @@ -182,25 +171,4 @@ class RecentChaptersPresenter( fun downloadChapters(items: List) { items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) } } - - /** - * Delete selected chapters - * - * @param items chapters selected - */ - private fun deleteChaptersInternal(chapterItems: List) { - val itemsByManga = chapterItems.groupBy { it.manga.id } - for ((_, items) in itemsByManga) { - val manga = items.first().manga - val source = sourceManager.get(manga.source) ?: continue - val chapters = items.map { it.chapter } - - downloadManager.deleteChapters(chapters, manga, source) - items.forEach { - it.status = Download.NOT_DOWNLOADED - it.download = null - } - } - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt index 962cf5f617..671766923f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt @@ -18,8 +18,8 @@ import java.text.DecimalFormatSymbols * @param controller a RecentlyReadController object * @constructor creates an instance of the adapter. */ -class RecentlyReadAdapter(controller: RecentlyReadController) -: FlexibleAdapter>(null, controller, true) { +class RecentlyReadAdapter(controller: RecentlyReadController) : + FlexibleAdapter>(null, controller, true) { val sourceManager by injectLazy() @@ -48,6 +48,6 @@ class RecentlyReadAdapter(controller: RecentlyReadController) } interface OnCoverClickListener { - fun onCoverClick(position: Int, lastTouchY: Float) + fun onCoverClick(position: Int) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt index 983cc5ab1e..9d4358d9d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt @@ -18,11 +18,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.browse.ProgressItem -import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import kotlinx.android.synthetic.main.recently_read_controller.* @@ -52,15 +53,14 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), * Endless loading item. */ private var progressItem: ProgressItem? = null - private var observeLater:Boolean = false + private var observeLater: Boolean = false private var query = "" private var presenter = RecentlyReadPresenter(this) private var recentItems: MutableList? = null - override fun getTitle(): String? { - return resources?.getString(R.string.label_recent_manga) + return resources?.getString(R.string.history) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -74,7 +74,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), */ override fun onViewCreated(view: View) { super.onViewCreated(view) - + // view.applyWindowInsetsForController() // Initialize adapter adapter = RecentlyReadAdapter(this) recycler.adapter = adapter @@ -82,6 +82,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), recycler.setHasFixedSize(true) recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) resetProgressItem() + scrollViewWith(recycler, padBottom = true) if (recentItems != null) adapter?.updateDataSet(recentItems!!.toList()) @@ -96,7 +97,11 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), override fun onActivityResumed(activity: Activity) { super.onActivityResumed(activity) if (observeLater) { - presenter.observe() + launchUI { + val manga = presenter.refresh(query) + recentItems = manga.toMutableList() + adapter?.updateDataSet(manga) + } observeLater = false } } @@ -123,7 +128,8 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), if (size > 0) { empty_view?.hide() } else { - empty_view.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga) + empty_view.show(R.drawable.ic_history_white_128dp, R.string + .no_recently_read_manga) } } @@ -157,7 +163,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), val intent = ReaderActivity.newIntent(activity, manga, nextChapter) startActivity(intent) } else { - activity.toast(R.string.no_next_chapter) + activity.toast(R.string.next_chapter_not_found) } } @@ -166,9 +172,9 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), RemoveHistoryDialog(this, manga, history).showDialog(router) } - override fun onCoverClick(position: Int, lastTouchY: Float) { + override fun onCoverClick(position: Int) { val manga = (adapter?.getItem(position) as? RecentlyReadItem)?.mch?.manga ?: return - router.pushController(MangaController(manga, lastTouchY).withFadeTransaction()) + router.pushController(MangaDetailsController(manga).withFadeTransaction()) } override fun removeHistory(manga: Manga, history: History, all: Boolean) { @@ -216,4 +222,16 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle), } }) } + + /*override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_recents -> { + router.setRoot( + RecentChaptersController().withFadeTransaction().tag(R.id.nav_recents.toString())) + Injekt.get().showRecentUpdates().set(true) + (activity as? MainActivity)?.updateRecentsIcon() + } + } + return super.onOptionsItemSelected(item) + }*/ } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt index cd0e7a8727..ae8e726143 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt @@ -1,7 +1,5 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.os.Build -import android.view.MotionEvent import android.view.View import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.kanade.tachiyomi.R @@ -11,7 +9,6 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.lang.toTimestampString import kotlinx.android.synthetic.main.recently_read_item.* import java.util.Date -import kotlin.math.max /** * Holder that contains recent manga item @@ -23,11 +20,10 @@ import kotlin.math.max * @constructor creates a new recent chapter holder. */ class RecentlyReadHolder( - view: View, - val adapter: RecentlyReadAdapter + view: View, + val adapter: RecentlyReadAdapter ) : BaseFlexibleViewHolder(view, adapter) { - private var lastTouchUpY = 0f init { remove.setOnClickListener { adapter.removeClickListener.onRemoveClick(adapterPosition) @@ -38,17 +34,7 @@ class RecentlyReadHolder( } cover.setOnClickListener { - adapter.coverClickListener.onCoverClick(adapterPosition, lastTouchUpY) - } - cover.setOnTouchListener { v, event -> - when (event?.action) { - MotionEvent.ACTION_UP -> { - val topH = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - v.rootWindowInsets.systemWindowInsetTop else 38 - lastTouchUpY = max(topH + 175f, event.rawY - topH - 154f) - } - } - false + adapter.coverClickListener.onCoverClick(adapterPosition) } } @@ -62,11 +48,11 @@ class RecentlyReadHolder( val (manga, chapter, history) = item // Set manga title - manga_title.text = manga.currentTitle() + manga_full_title.text = manga.title // Set source + chapter title val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - manga_source.text = itemView.context.getString(R.string.recent_manga_source) + manga_source.text = itemView.context.getString(R.string.source_dash_chapter_) .format(adapter.sourceManager.getOrStub(manga.source).toString(), formattedNumber) // Set last read timestamp title @@ -82,6 +68,4 @@ class RecentlyReadHolder( .into(cover) } } - - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt index 633903fd52..527a4f21bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt @@ -18,10 +18,12 @@ class RecentlyReadItem(val mch: MangaChapterHistory) : AbstractFlexibleItem>, - holder: RecentlyReadHolder, - position: Int, - payloads: MutableList?) { + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: RecentlyReadHolder, + position: Int, + payloads: MutableList? + ) { holder.bind(mch) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt index bcf73ba09b..0e025d639f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt @@ -1,19 +1,13 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.os.Bundle import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.launchUI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okhttp3.Dispatcher -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy import java.util.Calendar import java.util.Comparator @@ -30,7 +24,6 @@ class RecentlyReadPresenter(private val view: RecentlyReadController) { * Used to connect to database */ val db: DatabaseHelper by injectLazy() - private var readerSubscription:Subscription? = null var lastCount = 25 var lastSearch = "" @@ -44,29 +37,16 @@ class RecentlyReadPresenter(private val view: RecentlyReadController) { * Get all recent manga up to a point * @return list of history */ - private fun getRecentMangaLimit(search: String = ""): List { + private suspend fun getRecentMangaLimit(search: String = ""): List { // Set date for recent manga val cal = Calendar.getInstance() cal.time = Date() cal.add(Calendar.YEAR, -50) - return db.getRecentMangaLimit(cal.time, lastCount, search).executeAsBlocking() + return db.getRecentMangaLimit(cal.time, lastCount, search).executeOnIO() .map(::RecentlyReadItem) } - fun observe() { - readerSubscription?.unsubscribe() - val cal = Calendar.getInstance() - cal.time = Date() - cal.add(Calendar.YEAR, -50) - readerSubscription = db.getRecentMangaLimit(cal.time, lastCount, "").asRxObservable().map { - val items = it.map(::RecentlyReadItem) - launchUI { - view.onNextManga(items) - } - }.observeOn(Schedulers.io()).skip(1).take(1).subscribe() - } - /** * Reset last read of chapter to 0L * @param history history belonging to chapter @@ -78,7 +58,7 @@ class RecentlyReadPresenter(private val view: RecentlyReadController) { } suspend fun refresh(search: String? = null): List { - val manga = withContext(Dispatchers.IO) { getRecentMangaLimit(search ?: "") } + val manga = getRecentMangaLimit(search ?: "") checkIfNew(manga.size, search) lastSearch = search ?: lastSearch lastCount = manga.size @@ -147,5 +127,4 @@ class RecentlyReadPresenter(private val view: RecentlyReadController) { else -> throw NotImplementedError("Unknown sorting method") } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt index e3bea08f14..c6546dc45c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt @@ -5,38 +5,44 @@ import android.os.Bundle import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked -import com.afollestad.materialdialogs.customview.customView import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.DialogCheckboxView class RemoveHistoryDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T: RemoveHistoryDialog.Listener { + where T : Controller, T : RemoveHistoryDialog.Listener { private var manga: Manga? = null private var history: History? = null - constructor(target: T, manga: Manga, history: History) : this() { + private var chapter: Chapter? = null + + constructor(target: T, manga: Manga, history: History, chapter: Chapter? = null) : this() { this.manga = manga this.history = history + this.chapter = chapter targetController = target } override fun onCreateDialog(savedViewState: Bundle?): Dialog { val activity = activity!! - return MaterialDialog(activity) - .title(R.string.action_remove) - .message(R.string.dialog_with_checkbox_remove_description) - .checkBoxPrompt(res = R.string.dialog_with_checkbox_reset){} - .negativeButton(android.R.string.cancel) - .positiveButton(R.string.action_remove) { - onPositive(it.isCheckPromptChecked()) - } + return MaterialDialog(activity).title(R.string.reset_chapter_question).message( + text = if (chapter?.name != null) activity.getString( + R.string.this_will_remove_the_read_date_for_x_question, chapter?.name ?: "" + ) + else activity.getString(R.string.this_will_remove_the_read_date_question) + ).checkBoxPrompt( + text = activity.getString( + R.string.reset_all_chapters_for_this_, manga!!.mangaType(activity) + ) + ) {}.negativeButton(android.R.string.cancel).positiveButton(R.string.reset) { + onPositive(it.isCheckPromptChecked()) + } } private fun onPositive(checked: Boolean) { @@ -50,5 +56,4 @@ class RemoveHistoryDialog(bundle: Bundle? = null) : DialogController(bundle) interface Listener { fun removeHistory(manga: Manga, history: History, all: Boolean) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt new file mode 100644 index 0000000000..94e1fd4d26 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.recents + +import androidx.recyclerview.widget.ItemTouchHelper +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + +class RecentMangaAdapter(val delegate: RecentsInterface) : + BaseChapterAdapter>(delegate) { + + private var isAnimating = false + init { + setDisplayHeadersAtStartUp(true) + } + + fun updateItems(items: List>?) { + updateDataSet(items, isAnimating) + isAnimating = false + } + + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + interface RecentsInterface : RecentMangaInterface, DownloadInterface + + interface RecentMangaInterface { + fun onCoverClick(position: Int) + fun markAsRead(position: Int) + fun isSearching(): Boolean + fun showHistory() + fun showUpdates() + } + + override fun onItemSwiped(position: Int, direction: Int) { + super.onItemSwiped(position, direction) + isAnimating = true + when (direction) { + ItemTouchHelper.LEFT -> delegate.markAsRead(position) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHeaderItem.kt new file mode 100644 index 0000000000..785c3a6467 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHeaderItem.kt @@ -0,0 +1,83 @@ +package eu.kanade.tachiyomi.ui.recents + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.library.LibraryHeaderItem +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.recents_header_item.* + +class RecentMangaHeaderItem(val recentsType: Int) : + AbstractHeaderItem() { + + override fun getLayoutRes(): Int { + return R.layout.recents_header_item + } + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): Holder { + return Holder(view, adapter as RecentMangaAdapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: Holder, + position: Int, + payloads: MutableList? + ) { + holder.bind(recentsType) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is LibraryHeaderItem) { + return recentsType == recentsType + } + return false + } + + override fun isDraggable(): Boolean { + return false + } + + override fun isSwipeable(): Boolean { + return false + } + + override fun hashCode(): Int { + return recentsType.hashCode() + } + + class Holder(val view: View, adapter: RecentMangaAdapter) : BaseFlexibleViewHolder(view, adapter, + true) { + + init { + action_history.setOnClickListener { adapter.delegate.showHistory() } + action_update.setOnClickListener { adapter.delegate.showUpdates() } + } + + fun bind(recentsType: Int) { + title.setText(when (recentsType) { + CONTINUE_READING -> R.string.continue_reading + NEW_CHAPTERS -> R.string.new_chapters + NEWLY_ADDED -> R.string.newly_added + else -> R.string.continue_reading + }) + action_history.visibleIf(recentsType == -1) + action_update.visibleIf(recentsType == -1) + title.visibleIf(recentsType != -1) + } + } + + companion object { + const val CONTINUE_READING = 0 + const val NEW_CHAPTERS = 1 + const val NEWLY_ADDED = 2 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt new file mode 100644 index 0000000000..1485cdb0b8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt @@ -0,0 +1,115 @@ +package eu.kanade.tachiyomi.ui.recents + +import android.text.format.DateUtils +import android.view.View +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.signature.ObjectKey +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.download_button.* +import kotlinx.android.synthetic.main.recent_manga_item.* +import java.util.Date + +class RecentMangaHolder( + view: View, + val adapter: RecentMangaAdapter +) : BaseChapterHolder(view, adapter) { + + init { + cover_thumbnail?.setOnClickListener { adapter.delegate.onCoverClick(adapterPosition) } + } + + fun bind(recentsType: Int) { + when (recentsType) { + RecentMangaHeaderItem.CONTINUE_READING -> { + title.setText(R.string.view_history) + } + RecentMangaHeaderItem.NEW_CHAPTERS -> { + title.setText(R.string.view_all_updates) + } + } + } + + fun bind(item: RecentMangaItem) { + download_button.visibleIf(item.mch.manga.source != LocalSource.ID) + title.apply { + text = item.chapter.name + setTextColor(when { + item.chapter.bookmark -> context.getResourceColor(R.attr.colorAccent) + item.chapter.read -> context.getResourceColor(android.R.attr.textColorHint) + else -> context.getResourceColor(android.R.attr.textColorPrimary) + }) + } + subtitle.apply { + text = item.mch.manga.title + setTextColor(when { + item.chapter.read -> context.getResourceColor(android.R.attr.textColorHint) + else -> context.getResourceColor(android.R.attr.textColorPrimary) + }) + } + val notValidNum = item.mch.chapter.chapter_number <= 0 + body.text = when { + item.mch.chapter.id == null -> body.context.getString( + R.string.added_, DateUtils.getRelativeTimeSpanString( + item.mch.manga.date_added, Date().time, DateUtils.MINUTE_IN_MILLIS + ).toString() + ) + item.mch.history.id == null -> body.context.getString( + R.string.updated_, DateUtils.getRelativeTimeSpanString( + item.chapter.date_upload, Date().time, DateUtils.HOUR_IN_MILLIS + ).toString() + ) + item.chapter.id != item.mch.chapter.id -> + body.context.getString( + R.string.read_, DateUtils.getRelativeTimeSpanString( + item.mch.history.last_read, Date().time, DateUtils.MINUTE_IN_MILLIS + ).toString() + ) + "\n" + body.context.getString( + if (notValidNum) R.string.last_read_ else R.string.last_read_chapter_, + if (notValidNum) item.mch.chapter.name else adapter.decimalFormat.format(item.mch.chapter.chapter_number) + ) + item.chapter.pages_left > 0 && !item.chapter.read -> body.context.getString( + R.string.read_, DateUtils.getRelativeTimeSpanString( + item.mch.history.last_read, Date().time, DateUtils.MINUTE_IN_MILLIS + ).toString() + ) + "\n" + itemView.resources.getQuantityString( + R.plurals.pages_left, item.chapter.pages_left, item.chapter.pages_left + ) + else -> body.context.getString( + R.string.read_, DateUtils.getRelativeTimeSpanString( + item.mch.history.last_read, Date().time, DateUtils.MINUTE_IN_MILLIS + ).toString() + ) + } + GlideApp.with(itemView.context).load(item.mch.manga).diskCacheStrategy(DiskCacheStrategy + .AUTOMATIC) + .signature(ObjectKey(MangaImpl.getLastCoverFetch(item.mch.manga.id!!).toString())).into(cover_thumbnail) + notifyStatus( + if (adapter.isSelected(adapterPosition)) Download.CHECKED else item.status, + item.progress + ) + } + + override fun onLongClick(view: View?): Boolean { + super.onLongClick(view) + val item = adapter.getItem(adapterPosition) as? RecentMangaItem ?: return false + return item.mch.history.id != null + } + + fun notifyStatus(status: Int, progress: Int) = + download_button.setDownloadStatus(status, progress) + + override fun getFrontView(): View { + return front_view + } + + override fun getRearRightView(): View { + return right_view + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt new file mode 100644 index 0000000000..21698e4237 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.ui.recents + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.ChapterImpl +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterItem + +class RecentMangaItem( + val mch: MangaChapterHistory = MangaChapterHistory.createBlank(), + chapter: Chapter = ChapterImpl(), + header: AbstractHeaderItem<*>? +) : + BaseChapterItem>(chapter, header) { + + override fun getLayoutRes(): Int { + return if (mch.manga.id == null) R.layout.recents_footer_item + else R.layout.recent_manga_item + } + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): RecentMangaHolder { + return RecentMangaHolder(view, adapter as RecentMangaAdapter) + } + + override fun isSwipeable(): Boolean { + return mch.manga.id != null && !chapter.read + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is RecentMangaItem) { + return if (mch.manga.id == null) (header as? RecentMangaHeaderItem)?.recentsType == + (other.header as? RecentMangaHeaderItem)?.recentsType + else chapter.id == other.chapter.id + } + return false + } + + override fun hashCode(): Int { + return if (mch.manga.id == null) -((header as? RecentMangaHeaderItem)?.recentsType ?: 0).hashCode() + else (chapter.id ?: 0L).hashCode() + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: RecentMangaHolder, + position: Int, + payloads: MutableList? + ) { + if (mch.manga.id == null) holder.bind((header as? RecentMangaHeaderItem)?.recentsType ?: 0) + else holder.bind(this) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt new file mode 100644 index 0000000000..94b7050dce --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -0,0 +1,442 @@ +package eu.kanade.tachiyomi.ui.recents + +import android.app.Activity +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.widget.SearchView +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bluelinelabs.conductor.Controller +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.ui.base.controller.BaseController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.main.BottomSheetController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.main.RootSearchInterface +import eu.kanade.tachiyomi.ui.manga.MangaDetailsController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController +import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController +import eu.kanade.tachiyomi.ui.recently_read.RemoveHistoryDialog +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController +import eu.kanade.tachiyomi.util.view.scrollViewWith +import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener +import eu.kanade.tachiyomi.util.view.setStyle +import eu.kanade.tachiyomi.util.view.snack +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.updatePaddingRelative +import kotlinx.android.synthetic.main.download_bottom_sheet.* +import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.recents_controller.* +import kotlin.math.abs +import kotlin.math.max + +/** + * Fragment that shows recently read manga. + * Uses R.layout.fragment_recently_read. + * UI related actions should be called from here. + */ +class RecentsController(bundle: Bundle? = null) : BaseController(bundle), + RecentMangaAdapter.RecentsInterface, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + FlexibleAdapter.OnItemMoveListener, + RootSearchInterface, + BottomSheetController, + RemoveHistoryDialog.Listener { + + init { + setHasOptionsMenu(true) + retainViewMode = RetainViewMode.RETAIN_DETACH + } + + /** + * Adapter containing the recent manga. + */ + private var adapter = RecentMangaAdapter(this) + + private var presenter = RecentsPresenter(this) + private var snack: Snackbar? = null + private var lastChapterId: Long? = null + private var showingDownloads = false + var headerHeight = 0 + + override fun getTitle(): String? { + return if (showingDownloads) + resources?.getString(R.string.download_queue) + else resources?.getString(R.string.recents) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.recents_controller, container, false) + } + + /** + * Called when view is created + * + * @param view created view + */ + override fun onViewCreated(view: View) { + super.onViewCreated(view) + view.applyWindowInsetsForRootController(activity!!.bottom_nav) + // Initialize adapter + adapter = RecentMangaAdapter(this) + recycler.adapter = adapter + recycler.layoutManager = LinearLayoutManager(view.context) + recycler.setHasFixedSize(true) + recycler.recycledViewPool.setMaxRecycledViews(0, 0) + adapter.isSwipeEnabled = true + adapter.itemTouchHelperCallback.setSwipeFlags( + ItemTouchHelper.LEFT + ) + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = view.context.obtainStyledAttributes(attrsArray) + val appBarHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + swipe_refresh.setStyle() + scrollViewWith(recycler, skipFirstSnap = true, swipeRefreshLayout = swipe_refresh) { + headerHeight = it.systemWindowInsetTop + appBarHeight + } + + presenter.onCreate() + if (presenter.recentItems.isNotEmpty()) { + adapter.updateDataSet(presenter.recentItems) + adapter.addScrollableHeader(presenter.generalHeader) + } + + dl_bottom_sheet.onCreate(this) + + shadow2.alpha = + if (dl_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_COLLAPSED) 0.25f else 0f + shadow.alpha = + if (dl_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 0f + + dl_bottom_sheet.sheetBehavior?.addBottomSheetCallback(object : + BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { + shadow2.alpha = (1 - abs(progress)) * 0.25f + shadow.alpha = (1 - abs(progress)) * 0.5f + if (progress >= 0) activity?.appbar?.elevation = max( + progress * 15f, if (recycler.canScrollVertically(-1)) 15f else 0f + ) + sheet_layout.alpha = 1 - progress + activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress)) + val oldShow = showingDownloads + showingDownloads = progress > 0.92f + if (oldShow != showingDownloads) { + setTitle() + activity?.invalidateOptionsMenu() + } + } + + override fun onStateChanged(p0: View, state: Int) { + if (this@RecentsController.view == null) return + if (state == BottomSheetBehavior.STATE_EXPANDED) activity?.appbar?.y = 0f + if (state == BottomSheetBehavior.STATE_EXPANDED || state == BottomSheetBehavior.STATE_COLLAPSED) { + sheet_layout.alpha = + if (state == BottomSheetBehavior.STATE_COLLAPSED) 1f else 0f + showingDownloads = state == BottomSheetBehavior.STATE_EXPANDED + setTitle() + activity?.invalidateOptionsMenu() + } + + if (state == BottomSheetBehavior.STATE_HIDDEN || state == BottomSheetBehavior.STATE_COLLAPSED) { + shadow2.alpha = if (state == BottomSheetBehavior.STATE_COLLAPSED) 0.25f else 0f + shadow.alpha = if (state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 0f + } + + sheet_layout?.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED + sheet_layout?.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED + setPadding(dl_bottom_sheet.sheetBehavior?.isHideable == true) + } + }) + swipe_refresh.isRefreshing = LibraryUpdateService.isRunning() + swipe_refresh.setOnRefreshListener { + if (!LibraryUpdateService.isRunning()) { + LibraryUpdateService.start(view.context) + } + } + + if (showingDownloads) { + dl_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + setPadding(dl_bottom_sheet.sheetBehavior?.isHideable == true) + } + + fun reEnableSwipe() { + swipe_refresh.isRefreshing = false + } + + override fun onItemMove(fromPosition: Int, toPosition: Int) { } + + override fun shouldMoveItem(fromPosition: Int, toPosition: Int) = true + + override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + swipe_refresh.isEnabled = actionState != ItemTouchHelper.ACTION_STATE_SWIPE || + swipe_refresh.isRefreshing + } + + override fun handleSheetBack(): Boolean { + if (dl_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) { + dl_bottom_sheet.dismiss() + return true + } + return false + } + + fun setPadding(sheetIsHidden: Boolean) { + recycler.updatePaddingRelative(bottom = if (sheetIsHidden) 0 else 20.dpToPx) + recycler.updateLayoutParams { + bottomMargin = if (sheetIsHidden) 0 else 30.dpToPx + } + } + + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + if (view != null) { + refresh() + dl_bottom_sheet?.update() + } + } + + override fun onDestroy() { + super.onDestroy() + snack?.dismiss() + presenter.onDestroy() + snack = null + } + + fun refresh() = presenter.getRecents() + + fun showLists(recents: List) { + swipe_refresh.isRefreshing = LibraryUpdateService.isRunning() + adapter.updateItems(recents) + adapter.removeAllScrollableHeaders() + if (presenter.viewType > 0) + adapter.addScrollableHeader(presenter.generalHeader) + if (lastChapterId != null) { + refreshItem(lastChapterId ?: 0L) + lastChapterId = null + } + } + + fun updateChapterDownload(download: Download) { + if (view == null) return + dl_bottom_sheet.update() + dl_bottom_sheet.onUpdateProgress(download) + dl_bottom_sheet.onUpdateDownloadedPages(download) + val id = download.chapter.id ?: return + val holder = recycler.findViewHolderForItemId(id) as? RecentMangaHolder ?: return + holder.notifyStatus(download.status, download.progress) + } + + private fun refreshItem(chapterId: Long) { + val recentItemPos = adapter.currentItems.indexOfFirst { + it is RecentMangaItem && + it.mch.chapter.id == chapterId } + if (recentItemPos > -1) adapter.notifyItemChanged(recentItemPos) + } + + override fun downloadChapter(position: Int) { + val view = view ?: return + val item = adapter.getItem(position) as? RecentMangaItem ?: return + val chapter = item.chapter + val manga = item.mch.manga + if (item.status != Download.NOT_DOWNLOADED && item.status != Download.ERROR) { + presenter.deleteChapter(chapter, manga) + } else { + if (item.status == Download.ERROR) DownloadService.start(view.context) + else presenter.downloadChapter(manga, chapter) + } + } + + override fun startDownloadNow(position: Int) { + val chapter = (adapter.getItem(position) as? RecentMangaItem)?.chapter ?: return + presenter.startDownloadChapterNow(chapter) + } + + override fun onCoverClick(position: Int) { + val manga = (adapter.getItem(position) as? RecentMangaItem)?.mch?.manga ?: return + router.pushController(MangaDetailsController(manga).withFadeTransaction()) + } + + override fun showHistory() = router.pushController(RecentlyReadController().withFadeTransaction()) + override fun showUpdates() = router.pushController(RecentChaptersController().withFadeTransaction()) + + override fun onItemClick(view: View?, position: Int): Boolean { + val item = adapter.getItem(position) ?: return false + if (item is RecentMangaItem) { + if (item.mch.manga.id == null) { + val headerItem = adapter.getHeaderOf(item) as? RecentMangaHeaderItem + val controller: Controller = when (headerItem?.recentsType) { + RecentMangaHeaderItem.NEW_CHAPTERS -> RecentChaptersController() + RecentMangaHeaderItem.CONTINUE_READING -> RecentlyReadController() + else -> return false + } + router.pushController(controller.withFadeTransaction()) + } else { + val activity = activity ?: return false + val intent = ReaderActivity.newIntent(activity, item.mch.manga, item.chapter) + startActivity(intent) + } + } else if (item is RecentMangaHeaderItem) return false + return true + } + + override fun onItemLongClick(position: Int) { + val item = adapter.getItem(position) as? RecentMangaItem ?: return + val manga = item.mch.manga + val history = item.mch.history + val chapter = item.mch.chapter + if (history.id != null) + RemoveHistoryDialog(this, manga, history, chapter).showDialog(router) + } + + override fun removeHistory(manga: Manga, history: History, all: Boolean) { + if (all) { + // Reset last read of chapter to 0L + presenter.removeAllFromHistory(manga.id!!) + } else { + // Remove all chapters belonging to manga from library + presenter.removeFromHistory(history) + } + } + + override fun markAsRead(position: Int) { + val item = adapter.getItem(position) as? RecentMangaItem ?: return + val chapter = item.chapter + val manga = item.mch.manga + val lastRead = chapter.last_page_read + val pagesLeft = chapter.pages_left + lastChapterId = chapter.id + presenter.markChapterRead(chapter, true) + snack = view?.snack(R.string.marked_as_read, Snackbar.LENGTH_INDEFINITE) { + anchorView = activity?.bottom_nav + var undoing = false + setAction(R.string.undo) { + presenter.markChapterRead(chapter, false, lastRead, pagesLeft) + undoing = true + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!undoing && presenter.preferences.removeAfterMarkedAsRead()) { + lastChapterId = chapter.id + presenter.deleteChapter(chapter, manga) + } + } + }) + } + (activity as? MainActivity)?.setUndoSnackBar(snack) + } + + override fun isSearching() = presenter.query.isNotEmpty() + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (onRoot) (activity as? MainActivity)?.setDismissIcon(showingDownloads) + if (showingDownloads) { + inflater.inflate(R.menu.download_queue, menu) + } else { + inflater.inflate(R.menu.recents, menu) + + when (presenter.viewType) { + 0 -> menu.findItem(R.id.action_group_all) + 1 -> menu.findItem(R.id.action_ungroup_all) + 2 -> menu.findItem(R.id.action_only_history) + 3 -> menu.findItem(R.id.action_only_updates) + else -> null + }?.isChecked = true + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = view?.context?.getString(R.string.search_recents) + if (presenter.query.isNotEmpty()) { + searchItem.expandActionView() + searchView.setQuery(presenter.query, true) + searchView.clearFocus() + } + setOnQueryTextChangeListener(searchView) { + if (presenter.query != it) { + presenter.query = it ?: return@setOnQueryTextChangeListener false + refresh() + } + true + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + if (showingDownloads) dl_bottom_sheet.prepareMenu(menu) + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type.isEnter) { + view?.applyWindowInsetsForRootController(activity!!.bottom_nav) + if (type == ControllerChangeType.POP_ENTER) presenter.onCreate() + dl_bottom_sheet.dismiss() + } else { + if (type == ControllerChangeType.POP_EXIT) presenter.onDestroy() + snack?.dismiss() + } + } + + override fun showSheet() { + if (dl_bottom_sheet.sheetBehavior?.isHideable == false || presenter.downloadManager.hasQueue()) + dl_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun toggleSheet() { + if (showingDownloads) dl_bottom_sheet.dismiss() + else if (dl_bottom_sheet.sheetBehavior?.isHideable == false) dl_bottom_sheet.sheetBehavior?.state = + BottomSheetBehavior.STATE_EXPANDED + } + + override fun expandSearch() { + if (showingDownloads) { + dl_bottom_sheet.dismiss() + } else + activity?.toolbar?.menu?.findItem(R.id.action_search)?.expandActionView() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (showingDownloads) + return dl_bottom_sheet.onOptionsItemSelected(item) + when (item.itemId) { + R.id.action_group_all, R.id.action_ungroup_all, R.id.action_only_history, + R.id.action_only_updates -> { + presenter.toggleGroupRecents(when (item.itemId) { + R.id.action_ungroup_all -> 1 + R.id.action_only_history -> 2 + R.id.action_only_updates -> 3 + else -> 0 + }) + if (item.itemId == R.id.action_only_history) + activity?.toast(R.string.press_and_hold_to_reset_history, Toast.LENGTH_LONG) + activity?.invalidateOptionsMenu() + } + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt new file mode 100644 index 0000000000..58f39ff26b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -0,0 +1,345 @@ +package eu.kanade.tachiyomi.ui.recents + +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.HistoryImpl +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.download.model.DownloadQueue +import eu.kanade.tachiyomi.data.library.LibraryServiceListener +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.recent_updates.DateItem +import eu.kanade.tachiyomi.util.system.executeOnIO +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Calendar +import java.util.Date +import java.util.TreeMap +import java.util.concurrent.TimeUnit +import kotlin.math.abs + +class RecentsPresenter( + val controller: RecentsController, + val preferences: PreferencesHelper = Injekt.get(), + val downloadManager: DownloadManager = Injekt.get(), + private val db: DatabaseHelper = Injekt.get() +) : DownloadQueue.DownloadListener, LibraryServiceListener { + + private var scope = CoroutineScope(Job() + Dispatchers.Default) + + var recentItems = listOf() + private set + var query = "" + private val newAdditionsHeader = RecentMangaHeaderItem(RecentMangaHeaderItem.NEWLY_ADDED) + private val newChaptersHeader = RecentMangaHeaderItem(RecentMangaHeaderItem.NEW_CHAPTERS) + val generalHeader = RecentMangaHeaderItem(-1) + private val continueReadingHeader = RecentMangaHeaderItem(RecentMangaHeaderItem + .CONTINUE_READING) + var viewType: Int = preferences.recentsViewType().getOrDefault() + + fun onCreate() { + downloadManager.addListener(this) + LibraryUpdateService.setListener(this) + if (lastRecents != null) { + if (recentItems.isEmpty()) + recentItems = lastRecents ?: emptyList() + lastRecents = null + } + getRecents() + } + + fun getRecents() { + val oldQuery = query + scope.launch { + val isUngrouped = viewType > 0 && query.isEmpty() + // groupRecents && query.isEmpty() + val cal = Calendar.getInstance().apply { + time = Date() + when { + query.isNotEmpty() -> add(Calendar.YEAR, -50) + isUngrouped -> add(Calendar.MONTH, -1) + else -> add(Calendar.MONTH, -1) + } + } + + val calWeek = Calendar.getInstance().apply { + time = Date() + when { + query.isNotEmpty() -> add(Calendar.YEAR, -50) + isUngrouped -> add(Calendar.MONTH, -1) + else -> add(Calendar.WEEK_OF_YEAR, -1) + } + } + + val calDay = Calendar.getInstance().apply { + time = Date() + when { + query.isNotEmpty() -> add(Calendar.YEAR, -50) + isUngrouped -> add(Calendar.MONTH, -1) + else -> add(Calendar.DAY_OF_YEAR, -1) + } + } + + val cReading = if (viewType != 3) + if (query.isEmpty() && viewType != 2) + db.getRecentsWithUnread(cal.time, query, isUngrouped).executeOnIO() + else db.getRecentMangaLimit( + cal.time, + if (viewType == 2) 200 else 8, + query).executeOnIO() else emptyList() + val rUpdates = when { + viewType == 3 -> db.getRecentChapters(calWeek.time).executeOnIO().map { + MangaChapterHistory(it.manga, it.chapter, HistoryImpl()) + } + viewType != 2 -> db.getUpdatedManga(calWeek.time, query, isUngrouped).executeOnIO() + else -> emptyList() + } + rUpdates.forEach { + it.history.last_read = it.chapter.date_fetch + } + val nAdditions = if (viewType < 2) + db.getRecentlyAdded(calDay.time, query, isUngrouped).executeOnIO() else emptyList() + nAdditions.forEach { + it.history.last_read = it.manga.date_added + } + if (query != oldQuery) return@launch + val mangaList = (cReading + rUpdates + nAdditions).sortedByDescending { + it.history.last_read + }.distinctBy { + if (query.isEmpty() && viewType != 3) it.manga.id else it.chapter.id + } + val pairs = mangaList.mapNotNull { + val chapter = when { + viewType == 3 -> it.chapter + it.chapter.read || it.chapter.id == null -> getNextChapter(it.manga) + it.history.id == null -> getFirstUpdatedChapter(it.manga, it.chapter) + else -> it.chapter + } + if (chapter == null) if ((query.isNotEmpty() || viewType > 1) && + it.chapter.id != null) Pair(it, it.chapter) + else null + else Pair(it, chapter) + } + if (query.isEmpty() && !isUngrouped) { + val nChaptersItems = + pairs.filter { it.first.history.id == null && it.first.chapter.id != null } + .sortedWith(Comparator> { f1, f2 -> + if (abs(f1.second.date_fetch - f2.second.date_fetch) <= + TimeUnit.HOURS.toMillis(12)) + f2.second.date_upload.compareTo(f1.second.date_upload) + else + f2.second.date_fetch.compareTo(f1.second.date_fetch) + }) + .take(4).map { + RecentMangaItem( + it.first, + it.second, + newChaptersHeader + ) + } + + RecentMangaItem(header = newChaptersHeader) + val cReadingItems = + pairs.filter { it.first.history.id != null }.take(9 - nChaptersItems.size).map { + RecentMangaItem( + it.first, + it.second, + continueReadingHeader + ) + } + RecentMangaItem(header = continueReadingHeader) + val nAdditionsItems = pairs.filter { it.first.chapter.id == null }.take(4) + .map { RecentMangaItem(it.first, it.second, newAdditionsHeader) } + recentItems = + listOf(nChaptersItems, cReadingItems, nAdditionsItems).sortedByDescending { + it.firstOrNull()?.mch?.history?.last_read ?: 0L + }.flatten() + } else { + recentItems = + if (viewType == 3) { + val map = TreeMap>> { + d1, d2 -> d2 + .compareTo(d1) } + val byDay = + pairs.groupByTo(map, { getMapKey(it.first.history.last_read) }) + byDay.flatMap { + val dateItem = DateItem(it.key, true) + it.value.map { item -> + RecentMangaItem(item.first, item.second, dateItem) } + } + } else pairs.map { RecentMangaItem(it.first, it.second, null) } + if (isUngrouped && recentItems.isEmpty()) { + recentItems = listOf( + RecentMangaItem(header = newChaptersHeader), + RecentMangaItem(header = continueReadingHeader)) + } + } + setDownloadedChapters(recentItems) + withContext(Dispatchers.Main) { controller.showLists(recentItems) } + } + } + + private fun getNextChapter(manga: Manga): Chapter? { + val chapters = db.getChapters(manga).executeAsBlocking() + return chapters.sortedByDescending { it.source_order }.find { !it.read } + } + + private fun getFirstUpdatedChapter(manga: Manga, chapter: Chapter): Chapter? { + val chapters = db.getChapters(manga).executeAsBlocking() + return chapters.sortedByDescending { it.source_order }.find { + !it.read && abs(it.date_fetch - chapter.date_fetch) <= TimeUnit.HOURS.toMillis(12) + } + } + + fun onDestroy() { + downloadManager.removeListener(this) + LibraryUpdateService.removeListener(this) + lastRecents = recentItems + } + + fun cancelScope() { + scope.cancel() + } + + fun toggleGroupRecents(pref: Int) { + preferences.recentsViewType().set(pref) + viewType = pref + getRecents() + } + + /** + * Finds and assigns the list of downloaded chapters. + * + * @param chapters the list of chapter from the database. + */ + private fun setDownloadedChapters(chapters: List) { + for (item in chapters) { + if (downloadManager.isChapterDownloaded(item.chapter, item.mch.manga)) { + item.status = Download.DOWNLOADED + } else if (downloadManager.hasQueue()) { + item.status = downloadManager.queue.find { it.chapter.id == item.chapter.id } + ?.status ?: 0 + } + } + } + + override fun updateDownload(download: Download) { + recentItems.find { it.chapter.id == download.chapter.id }?.download = download + scope.launch(Dispatchers.Main) { + controller.updateChapterDownload(download) + } + } + + override fun onUpdateManga(manga: LibraryManga) { + if (manga.id == null) scope.launch(Dispatchers.Main) { controller.reEnableSwipe() } + else getRecents() + } + + /** + * Deletes the given list of chapter. + * @param chapter the chapter to delete. + */ + fun deleteChapter(chapter: Chapter, manga: Manga, update: Boolean = true) { + val source = Injekt.get().getOrStub(manga.source) + downloadManager.deleteChapters(listOf(chapter), manga, source) + + if (update) { + val item = recentItems.find { it.chapter.id == chapter.id } ?: return + item.apply { + status = Download.NOT_DOWNLOADED + download = null + } + + controller.showLists(recentItems) + } + } + + /** + * Get date as time key + * + * @param date desired date + * @return date as time key + */ + private fun getMapKey(date: Long): Date { + val cal = Calendar.getInstance() + cal.time = Date(date) + cal[Calendar.HOUR_OF_DAY] = 0 + cal[Calendar.MINUTE] = 0 + cal[Calendar.SECOND] = 0 + cal[Calendar.MILLISECOND] = 0 + return cal.time + } + + /** + * Downloads the given list of chapters with the manager. + * @param chapter the chapter to download. + */ + fun downloadChapter(manga: Manga, chapter: Chapter) { + downloadManager.downloadChapters(manga, listOf(chapter)) + } + + fun startDownloadChapterNow(chapter: Chapter) { + downloadManager.startDownloadNow(chapter) + } + + /** + * Mark the selected chapter list as read/unread. + * @param selectedChapters the list of selected chapters. + * @param read whether to mark chapters as read or unread. + */ + fun markChapterRead( + chapter: Chapter, + read: Boolean, + lastRead: Int? = null, + pagesLeft: Int? = null + ) { + scope.launch(Dispatchers.IO) { + chapter.apply { + this.read = read + if (!read) { + last_page_read = lastRead ?: 0 + pages_left = pagesLeft ?: 0 + } + } + db.updateChaptersProgress(listOf(chapter)).executeAsBlocking() + getRecents() + } + } + + // History + /** + * Reset last read of chapter to 0L + * @param history history belonging to chapter + */ + fun removeFromHistory(history: History) { + history.last_read = 0L + db.updateHistoryLastRead(history).executeAsBlocking() + getRecents() + } + + /** + * Removes all chapters belonging to manga from history. + * @param mangaId id of manga + */ + fun removeAllFromHistory(mangaId: Long) { + val history = db.getHistoryByMangaId(mangaId).executeAsBlocking() + history.forEach { it.last_read = 0L } + db.updateHistoryLastRead(history).executeAsBlocking() + getRecents() + } + + companion object { + var lastRecents: List? = null + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/BiometricActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricActivity.kt similarity index 71% rename from app/src/main/java/eu/kanade/tachiyomi/ui/main/BiometricActivity.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricActivity.kt index b630cb849d..944b3e1f3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/BiometricActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricActivity.kt @@ -1,38 +1,34 @@ -package eu.kanade.tachiyomi.ui.main +package eu.kanade.tachiyomi.ui.security import android.os.Bundle import androidx.biometric.BiometricPrompt import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import uy.kohesive.injekt.injectLazy import java.util.Date +import java.util.concurrent.ExecutorService import java.util.concurrent.Executors class BiometricActivity : BaseActivity() { - val executor = Executors.newSingleThreadExecutor() + private val executor: ExecutorService = Executors.newSingleThreadExecutor() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val fromSearch = intent.getBooleanExtra("fromSearch", false) val biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt .AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) - finishAffinity() + if (fromSearch) finish() + else finishAffinity() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) - MainActivity.unlocked = true + SecureActivityDelegate.locked = false preferences.lastUnlock().set(Date().time) finish() } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - // TODO("Called when a biometric is valid but not recognized.") - } }) val promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -42,5 +38,4 @@ class BiometricActivity : BaseActivity() { biometricPrompt.authenticate(promptInfo) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt new file mode 100644 index 0000000000..d37ea8bd69 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.ui.security + +import android.app.Activity +import android.content.Intent +import android.view.WindowManager +import androidx.biometric.BiometricManager +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.main.SearchActivity +import uy.kohesive.injekt.injectLazy +import java.util.Date + +object SecureActivityDelegate { + + private val preferences by injectLazy() + + var locked: Boolean = true + + fun setSecure(activity: Activity?, force: Boolean? = null) { + val enabled = force ?: preferences.secureScreen().getOrDefault() + if (enabled) { + activity?.window?.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } else { + activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + fun promptLockIfNeeded(activity: Activity?) { + if (activity == null) return + val lockApp = preferences.useBiometrics().getOrDefault() + if (lockApp && BiometricManager.from(activity).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { + if (isAppLocked()) { + val intent = Intent(activity, BiometricActivity::class.java) + intent.putExtra("fromSearch", (activity is SearchActivity)) + activity.startActivity(intent) + activity.overridePendingTransition(0, 0) + } + } else if (lockApp) { + preferences.useBiometrics().set(false) + } + } + + fun shouldBeLocked(): Boolean { + val lockApp = preferences.useBiometrics().getOrDefault() + if (lockApp && isAppLocked()) return true + return false + } + + private fun isAppLocked(): Boolean { + return locked && + (preferences.lockAfter().getOrDefault() <= 0 || + Date().time >= preferences.lastUnlock().getOrDefault() + 60 * 1000 * preferences + .lockAfter().getOrDefault()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index 58037acb7f..2e38b73590 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -1,21 +1,21 @@ package eu.kanade.tachiyomi.ui.setting import android.app.Activity -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import androidx.core.graphics.drawable.DrawableCompat -import androidx.preference.* -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.list.listItems -import com.afollestad.materialdialogs.list.listItemsSingleChoice -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault +import androidx.preference.CheckBoxPreference +import androidx.preference.DialogPreference +import androidx.preference.DropDownPreference +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceGroup +import androidx.preference.PreferenceManager +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import eu.kanade.tachiyomi.widget.preference.IntListMatPreference -import eu.kanade.tachiyomi.widget.preference.IntListPreference import eu.kanade.tachiyomi.widget.preference.ListMatPreference import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy @DslMarker @Target(AnnotationTarget.TYPE) @@ -41,21 +41,35 @@ inline fun PreferenceGroup.editTextPreference(block: (@DSL EditTextPreference).( return initThenAdd(EditTextPreference(context), block).also(::initDialog) } -inline fun PreferenceGroup.listPreference(activity: Activity?, block: (@DSL ListMatPreference).() --> Unit): +inline fun PreferenceGroup.dropDownPreference(block: (@DSL DropDownPreference).() -> Unit): + DropDownPreference { + return initThenAdd(DropDownPreference(context), block).also(::initDialog) +} + +inline fun PreferenceGroup.listPreference( + activity: Activity?, + block: (@DSL ListMatPreference).() + -> Unit +): ListMatPreference { return initThenAdd(ListMatPreference(activity, context), block) } -inline fun PreferenceGroup.intListPreference(activity: Activity?, block: (@DSL -IntListMatPreference).() -> Unit): +inline fun PreferenceGroup.intListPreference( + activity: Activity?, + block: (@DSL + IntListMatPreference).() -> Unit +): IntListMatPreference { return initThenAdd(IntListMatPreference(activity, context), block) } -inline fun PreferenceGroup.multiSelectListPreferenceMat(activity: Activity?, block: (@DSL -MultiListMatPreference).() --> Unit): MultiListMatPreference { +inline fun PreferenceGroup.multiSelectListPreferenceMat( + activity: Activity?, + block: (@DSL + MultiListMatPreference).() + -> Unit +): MultiListMatPreference { return initThenAdd(MultiListMatPreference(activity, context), block) } @@ -87,7 +101,7 @@ inline fun

PreferenceGroup.initThenAdd(p: P, block: P.() -> Uni inline fun

PreferenceGroup.addThenInit(p: P, block: P.() -> Unit): P { return p.apply { - this.isIconSpaceReserved = false + this.isIconSpaceReserved = false addPreference(this) block() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt index 03622f7d5f..fde8943613 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt @@ -4,7 +4,6 @@ import android.app.Dialog import android.content.Intent import android.net.Uri import android.os.Bundle -import android.view.View import androidx.preference.PreferenceScreen import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.BuildConfig @@ -16,11 +15,13 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult import eu.kanade.tachiyomi.data.updater.UpdaterService import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.main.ChangelogDialogController -import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.lang.toTimestampString -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.text.DateFormat @@ -43,17 +44,17 @@ class SettingsAboutController : SettingsController() { /** * The subscribtion service of the obtained release object */ - private var releaseSubscription: Subscription? = null + private val scope = CoroutineScope(Job() + Dispatchers.IO) private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_about + titleRes = R.string.about switchPreference { key = "acra.enable" - titleRes = R.string.pref_enable_acra - summaryRes = R.string.pref_acra_summary + titleRes = R.string.send_crash_report + summaryRes = R.string.helps_fix_bugs defaultValue = true } preference { @@ -95,40 +96,37 @@ class SettingsAboutController : SettingsController() { } } - override fun onDestroyView(view: View) { - super.onDestroyView(view) - releaseSubscription?.unsubscribe() - releaseSubscription = null - } - /** * Checks version and shows a user prompt if an update is available. */ private fun checkVersion() { if (activity == null) return - activity?.toast(R.string.update_check_look_for_updates) - releaseSubscription?.unsubscribe() - releaseSubscription = updateChecker.checkForUpdate() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ result -> - when (result) { - is UpdateResult.NewUpdate<*> -> { - val body = result.release.info - val url = result.release.downloadLink - - // Create confirmation window - NewUpdateDialogController(body, url).showDialog(router) - } - is UpdateResult.NoNewUpdate -> { - activity?.toast(R.string.update_check_no_new_updates) - } + activity?.toast(R.string.searching_for_updates) + scope.launch { + val result = try { + updateChecker.checkForUpdate() + } catch (error: Exception) { + activity?.toast(error.message) + Timber.e(error) + } + when (result) { + is UpdateResult.NewUpdate<*> -> { + val body = result.release.info + val url = result.release.downloadLink + + // Create confirmation window + withContext(Dispatchers.Main) { + NewUpdateDialogController(body, url).showDialog(router) + } + } + is UpdateResult.NoNewUpdate -> { + withContext(Dispatchers.Main) { + activity?.toast(R.string.no_new_updates_available) } - }, { error -> - activity?.toast(error.message) - Timber.e(error) - }) + } + } + } } class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) { @@ -140,9 +138,9 @@ class SettingsAboutController : SettingsController() { override fun onCreateDialog(savedViewState: Bundle?): Dialog { return MaterialDialog(activity!!) - .title(R.string.update_check_title) + .title(R.string.new_version_available) .message(text = args.getString(BODY_KEY) ?: "") - .positiveButton(R.string.update_check_confirm) { + .positiveButton(R.string.download) { val appContext = applicationContext if (appContext != null) { // Start download @@ -150,7 +148,7 @@ class SettingsAboutController : SettingsController() { UpdaterService.downloadUpdate(appContext, url) } } - .negativeButton(R.string.update_check_ignore) + .negativeButton(R.string.ignore) } private companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index d476f18a0c..37a360f27d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -2,11 +2,9 @@ package eu.kanade.tachiyomi.ui.setting import android.app.Dialog import android.os.Bundle -import androidx.preference.PreferenceScreen import android.widget.Toast +import androidx.preference.PreferenceScreen import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -16,14 +14,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import eu.kanade.tachiyomi.util.system.toast import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -40,17 +37,17 @@ class SettingsAdvancedController : SettingsController() { private val db: DatabaseHelper by injectLazy() override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_advanced + titleRes = R.string.advanced preference { key = CLEAR_CACHE_KEY - titleRes = R.string.pref_clear_chapter_cache - summary = context.getString(R.string.used_cache, chapterCache.readableSize) + titleRes = R.string.clear_chapter_cache + summary = context.getString(R.string.used_, chapterCache.readableSize) onClick { clearChapterCache() } } preference { - titleRes = R.string.pref_clear_cookies + titleRes = R.string.clear_cookies onClick { network.cookieManager.removeAll() @@ -58,8 +55,8 @@ class SettingsAdvancedController : SettingsController() { } } preference { - titleRes = R.string.pref_clear_database - summaryRes = R.string.pref_clear_database_summary + titleRes = R.string.clear_database + summaryRes = R.string.clear_database_summary onClick { val ctrl = ClearDatabaseDialogController() @@ -68,21 +65,21 @@ class SettingsAdvancedController : SettingsController() { } } preference { - titleRes = R.string.pref_refresh_library_metadata - summaryRes = R.string.pref_refresh_library_metadata_summary + titleRes = R.string.refresh_library_metadata + summaryRes = R.string.updates_covers_genres_desc onClick { LibraryUpdateService.start(context, target = Target.DETAILS) } } preference { - titleRes = R.string.pref_refresh_library_tracking - summaryRes = R.string.pref_refresh_library_tracking_summary + titleRes = R.string.refresh_tracking_metadata + summaryRes = R.string.updates_tracking_details onClick { LibraryUpdateService.start(context, target = Target.TRACKING) } } preference { - titleRes = R.string.pref_clean_downloads + titleRes = R.string.clean_up_downloaded_chapters - summaryRes = R.string.pref_clean_downloads_summary + summaryRes = R.string.delete_unused_chapters onClick { cleanupDownloads() } } @@ -104,7 +101,7 @@ class SettingsAdvancedController : SettingsController() { launchUI { val activity = activity ?: return@launchUI val cleanupString = - if (foldersCleared == 0) activity.getString(R.string.no_cleanup_done) + if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup) else resources!!.getQuantityString( R.plurals.cleanup_done, foldersCleared, @@ -113,7 +110,6 @@ class SettingsAdvancedController : SettingsController() { activity.toast(cleanupString, Toast.LENGTH_LONG) } } - } private fun clearChapterCache() { @@ -134,10 +130,10 @@ class SettingsAdvancedController : SettingsController() { }, { activity?.toast(R.string.cache_delete_error) }, { - activity?.toast(resources?.getQuantityString(R.plurals.cache_deleted, + activity?.toast(resources?.getQuantityString(R.plurals.cache_cleared, deletedFiles, deletedFiles)) findPreference(CLEAR_CACHE_KEY)?.summary = - resources?.getString(R.string.used_cache, chapterCache.readableSize) + resources?.getString(R.string.used_, chapterCache.readableSize) }) } @@ -153,12 +149,6 @@ class SettingsAdvancedController : SettingsController() { } private fun clearDatabase() { - // Avoid weird behavior by going back to the library. - val newBackstack = listOf(RouterTransaction.with(LibraryController())) + - router.backstack.drop(1) - - router.setBackstack(newBackstack, FadeChangeHandler()) - db.deleteMangasNotInLibrary().executeAsBlocking() db.deleteHistoryNoLastRead().executeAsBlocking() activity?.toast(R.string.clear_database_completed) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt index 4989540ba0..f861fd5e60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt @@ -3,7 +3,11 @@ package eu.kanade.tachiyomi.ui.setting import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.app.Activity import android.app.Dialog -import android.content.* +import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.net.Uri import android.os.Bundle import android.view.View @@ -54,8 +58,8 @@ class SettingsBackupController : SettingsController() { titleRes = R.string.backup preference { - titleRes = R.string.pref_create_backup - summaryRes = R.string.pref_create_backup_summ + titleRes = R.string.create_backup + summaryRes = R.string.can_be_used_to_restore onClick { val ctrl = CreateBackupDialog() @@ -64,27 +68,27 @@ class SettingsBackupController : SettingsController() { } } preference { - titleRes = R.string.pref_restore_backup - summaryRes = R.string.pref_restore_backup_summ + titleRes = R.string.restore_backup + summaryRes = R.string.restore_from_backup_file onClick { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "application/*" - val title = resources?.getString(R.string.file_select_backup) + val title = resources?.getString(R.string.select_backup_file) val chooser = Intent.createChooser(intent, title) startActivityForResult(chooser, CODE_BACKUP_RESTORE) } } preferenceCategory { - titleRes = R.string.pref_backup_service_category + titleRes = R.string.service intListPreference(activity) { key = Keys.backupInterval - titleRes = R.string.pref_backup_interval - entriesRes = arrayOf(R.string.update_never, R.string.update_6hour, - R.string.update_12hour, R.string.update_24hour, - R.string.update_48hour, R.string.update_weekly) + titleRes = R.string.backup_frequency + entriesRes = arrayOf(R.string.manual, R.string.every_6_hours, + R.string.every_12_hours, R.string.daily, + R.string.every_2_days, R.string.weekly) entryValues = listOf(0, 6, 12, 24, 48, 168) defaultValue = 0 @@ -101,14 +105,14 @@ class SettingsBackupController : SettingsController() { } val backupDir = preference { key = Keys.backupDirectory - titleRes = R.string.pref_backup_directory + titleRes = R.string.backup_location onClick { val currentDir = preferences.backupsDirectory().getOrDefault() - try{ + try { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, CODE_BACKUP_DIR) - } catch (e: ActivityNotFoundException){ + } catch (e: ActivityNotFoundException) { // Fall back to custom picker on error startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR) } @@ -122,7 +126,7 @@ class SettingsBackupController : SettingsController() { } val backupNumber = intListPreference(activity) { key = Keys.numberOfBackups - titleRes = R.string.pref_backup_slots + titleRes = R.string.max_auto_backups entries = listOf("1", "2", "3", "4", "5") entryRange = 1..5 defaultValue = 1 @@ -134,7 +138,6 @@ class SettingsBackupController : SettingsController() { backupNumber.isVisible = it > 0 } } - } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -203,15 +206,14 @@ class SettingsBackupController : SettingsController() { override fun onCreateDialog(savedViewState: Bundle?): Dialog { val activity = activity!! val options = arrayOf(R.string.manga, R.string.categories, R.string.chapters, - R.string.track, R.string.history) + R.string.tracking, R.string.history) .map { activity.getString(it) } return MaterialDialog(activity) - .title(R.string.pref_create_backup) - .message(R.string.backup_choice) + .title(R.string.create_backup) + .message(R.string.what_should_backup) .listItemsMultiChoice(items = options, disabledIndices = intArrayOf(0), - initialSelection = intArrayOf(0)) - { _, positions, _ -> + initialSelection = intArrayOf(0)) { _, positions, _ -> var flags = 0 for (i in 1 until positions.size) { when (positions[i]) { @@ -224,7 +226,7 @@ class SettingsBackupController : SettingsController() { (targetController as? SettingsBackupController)?.createBackup(flags) } - .positiveButton(R.string.action_create) + .positiveButton(R.string.create) .negativeButton(android.R.string.cancel) } } @@ -240,9 +242,9 @@ class SettingsBackupController : SettingsController() { return MaterialDialog(activity).apply { title(R.string.backup_created) if (uniFile.filePath != null) - message(text = resources?.getString(R.string.file_saved, uniFile.filePath)) - positiveButton(R.string.action_close) - negativeButton(R.string.action_share) { + message(text = resources?.getString(R.string.file_saved_at_, uniFile.filePath)) + positiveButton(R.string.close) + negativeButton(R.string.share) { val sendIntent = Intent(Intent.ACTION_SEND) sendIntent.type = "application/json" sendIntent.putExtra(Intent.EXTRA_STREAM, uniFile.uri) @@ -263,9 +265,9 @@ class SettingsBackupController : SettingsController() { override fun onCreateDialog(savedViewState: Bundle?): Dialog { return MaterialDialog(activity!!) - .title(R.string.pref_restore_backup) - .message(R.string.backup_restore_content) - .positiveButton(R.string.action_restore) { + .title(R.string.restore_backup) + .message(R.string.restore_message) + .positiveButton(R.string.restore) { val context = applicationContext if (context != null) { activity?.toast(R.string.restoring_backup) @@ -307,5 +309,4 @@ class SettingsBackupController : SettingsController() { const val TAG_CREATING_BACKUP_DIALOG = "CreatingBackupDialog" const val TAG_RESTORING_BACKUP_DIALOG = "RestoringBackupDialog" } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt index 91c2156ea3..53df954425 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt @@ -2,22 +2,20 @@ package eu.kanade.tachiyomi.ui.setting import android.content.Context import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.preference.PreferenceController -import androidx.preference.PreferenceScreen import android.util.TypedValue import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceController +import androidx.preference.PreferenceScreen import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.BaseController -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets -import eu.kanade.tachiyomi.util.view.updatePaddingRelative +import eu.kanade.tachiyomi.util.view.scrollViewWith import rx.Observable import rx.Subscription import rx.subscriptions.CompositeSubscription @@ -36,7 +34,7 @@ abstract class SettingsController : PreferenceController() { untilDestroySubscriptions = CompositeSubscription() } val view = super.onCreateView(inflater, container, savedInstanceState) - listView.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + scrollViewWith(listView, padBottom = true) return view } @@ -79,6 +77,7 @@ abstract class SettingsController : PreferenceController() { if (type.isEnter) { setTitle() } + setHasOptionsMenu(type.isEnter) super.onChangeStarted(handler, type) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 83a548c566..f282d2f15a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -29,11 +29,11 @@ class SettingsDownloadController : SettingsController() { private val db: DatabaseHelper by injectLazy() override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_downloads + titleRes = R.string.downloads preference { key = Keys.downloadsDirectory - titleRes = R.string.pref_download_directory + titleRes = R.string.download_location onClick { val ctrl = DownloadDirectoriesDialog() ctrl.targetController = this@SettingsDownloadController @@ -48,21 +48,21 @@ class SettingsDownloadController : SettingsController() { } switchPreference { key = Keys.downloadOnlyOverWifi - titleRes = R.string.pref_download_only_over_wifi + titleRes = R.string.only_download_over_wifi defaultValue = true } preferenceCategory { - titleRes = R.string.pref_remove_after_read + titleRes = R.string.remove_after_read switchPreference { key = Keys.removeAfterMarkedAsRead - titleRes = R.string.pref_remove_after_marked_as_read + titleRes = R.string.remove_when_marked_as_read defaultValue = false } intListPreference(activity) { key = Keys.removeAfterReadSlots - titleRes = R.string.pref_remove_after_read - entriesRes = arrayOf(R.string.disabled, R.string.last_read_chapter, + titleRes = R.string.remove_after_read + entriesRes = arrayOf(R.string.never, R.string.last_read_chapter, R.string.second_to_last, R.string.third_to_last, R.string.fourth_to_last, R.string.fifth_to_last) entryRange = -1..4 @@ -73,16 +73,16 @@ class SettingsDownloadController : SettingsController() { val dbCategories = db.getCategories().executeAsBlocking() preferenceCategory { - titleRes = R.string.pref_download_new + titleRes = R.string.download_new_chapters switchPreference { key = Keys.downloadNew - titleRes = R.string.pref_download_new + titleRes = R.string.download_new_chapters defaultValue = false } multiSelectListPreferenceMat(activity) { key = Keys.downloadNewCategories - titleRes = R.string.pref_download_new_categories + titleRes = R.string.categories_to_include_in_download entries = dbCategories.map { it.name } entryValues = dbCategories.map { it.id.toString() } allSelectionRes = R.string.all @@ -145,12 +145,11 @@ class SettingsDownloadController : SettingsController() { override fun onCreateDialog(savedViewState: Bundle?): Dialog { val activity = activity!! val currentDir = preferences.downloadsDirectory().getOrDefault() - val externalDirs = getExternalDirs() + File(activity.getString(R.string.custom_dir)) + val externalDirs = getExternalDirs() + File(activity.getString(R.string.custom_location)) val selectedIndex = externalDirs.map(File::toString).indexOfFirst { it in currentDir } return MaterialDialog(activity) - .listItemsSingleChoice(items = externalDirs.map { it.path }, initialSelection = selectedIndex) - { _, position, text -> + .listItemsSingleChoice(items = externalDirs.map { it.path }, initialSelection = selectedIndex) { _, position, text -> val target = targetController as? SettingsDownloadController if (position == externalDirs.lastIndex) { target?.customDirectorySelected(currentDir) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt index 25929b4901..b99421b35a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -6,8 +6,9 @@ import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.updater.UpdaterJob -import eu.kanade.tachiyomi.widget.preference.IntListMatPreference +import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.widget.preference.IntListMatPreference import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsGeneralController : SettingsController() { @@ -15,18 +16,18 @@ class SettingsGeneralController : SettingsController() { private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_general + titleRes = R.string.general listPreference(activity) { key = Keys.lang - titleRes = R.string.pref_language + titleRes = R.string.language entryValues = listOf("", "ar", "bg", "bn", "ca", "cs", "de", "el", "en-US", "en-GB", "es", "fr", "hi", "hu", "in", "it", "ja", "ko", "lv", "ms", "nb-rNO", "nl", "pl", "pt", "pt-BR", "ro", "ru", "sc", "sr", "sv", "th", "tl", "tr", "uk", "vi", "zh-rCN") entries = entryValues.map { value -> val locale = LocaleHelper.getLocaleFromString(value.toString()) - locale?.getDisplayName(locale)?.capitalize() ?: - context.getString(R.string.system_default) + locale?.getDisplayName(locale)?.capitalize() + ?: context.getString(R.string.system_default) } defaultValue = "" summary = "%s" @@ -43,11 +44,12 @@ class SettingsGeneralController : SettingsController() { intListPreference(activity) { key = Keys.theme - titleRes = R.string.pref_theme - entriesRes = arrayOf(R.string.light_theme, R.string.dark_theme, - R.string.amoled_theme, R.string.darkblue_theme, - R.string.system_theme, R.string.system_amoled_theme, R.string.system_darkblue_theme) - entryRange = 1..7 + titleRes = R.string.app_theme + entriesRes = arrayOf(R.string.white_theme, R.string.light_blue, R.string.dark, + R.string.amoled_black, R.string.dark_blue, R.string.system_default, R.string + .system_default_amoled, + R.string.system_default_all_blue) + entryValues = listOf(1, 8, 2, 3, 4, 5, 6, 7) defaultValue = 5 onChange { @@ -55,9 +57,10 @@ class SettingsGeneralController : SettingsController() { true } } + listPreference(activity) { - key= Keys.dateFormat - titleRes = R.string.pref_date_format + key = Keys.dateFormat + titleRes = R.string.date_format entryValues = listOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd") entries = entryValues.map { value -> if (value == "") { @@ -69,18 +72,11 @@ class SettingsGeneralController : SettingsController() { defaultValue = "" summary = "%s" } - intListPreference(activity) { - key = Keys.startScreen - titleRes = R.string.pref_start_screen - entriesRes = arrayOf(R.string.label_library, R.string.label_recent_manga, - R.string.label_recent_updates) - entryRange = 1..3 - defaultValue = 1 - } + switchPreference { key = Keys.automaticUpdates - titleRes = R.string.pref_enable_automatic_updates - summaryRes = R.string.pref_enable_automatic_updates_summary + titleRes = R.string.check_for_updates + summaryRes = R.string.auto_check_for_app_versions defaultValue = true if (isUpdaterEnabled) { @@ -98,35 +94,53 @@ class SettingsGeneralController : SettingsController() { } } - val biometricManager = BiometricManager.from(context) - if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { - var preference:IntListMatPreference? = null + preferenceCategory { + titleRes = R.string.security + + val biometricManager = BiometricManager.from(context) + if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { + var preference: IntListMatPreference? = null + switchPreference { + key = Keys.useBiometrics + titleRes = R.string.lock_with_biometrics + defaultValue = false + + onChange { + preference?.isVisible = it as Boolean + true + } + } + preference = intListPreference(activity) { + key = Keys.lockAfter + titleRes = R.string.lock_when_idle + isVisible = preferences.useBiometrics().getOrDefault() + val values = listOf(0, 2, 5, 10, 20, 30, 60, 90, 120, -1) + entries = values.mapNotNull { + when (it) { + 0 -> context.getString(R.string.always) + -1 -> context.getString(R.string.never) + else -> resources?.getQuantityString( + R.plurals.after_minutes, it.toInt(), it + ) + } + } + entryValues = values + defaultValue = 0 + } + } + switchPreference { - key = Keys.useBiometrics - titleRes = R.string.lock_with_biometrics + key = Keys.secureScreen + titleRes = R.string.secure_screen + summaryRes = R.string.hide_tachi_from_recents defaultValue = false onChange { - preference?.isVisible = it as Boolean + it as Boolean + SecureActivityDelegate.setSecure(activity, it) true } } - preference = intListPreference(activity) { - key = Keys.lockAfter - titleRes = R.string.lock_when_idle - isVisible = preferences.useBiometrics().getOrDefault() - val values = listOf(0, 2, 5, 10, 20, 30, 60, 90, 120, -1) - entries = values.mapNotNull { - when (it) { - 0 -> context.getString(R.string.lock_always) - -1 -> context.getString(R.string.lock_never) - else -> resources?.getQuantityString(R.plurals.lock_after_mins, it.toInt(), - it) - } - } - entryValues = values - defaultValue = 0 - } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt index e5129bbee0..5f811fd136 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt @@ -1,21 +1,14 @@ package eu.kanade.tachiyomi.ui.setting -import android.app.Dialog -import android.os.Bundle import android.os.Handler -import android.view.View import androidx.preference.PreferenceScreen -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.customview.customView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import kotlinx.android.synthetic.main.pref_library_columns.view.* -import rx.Observable +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.category.CategoryController import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys @@ -25,37 +18,13 @@ class SettingsLibraryController : SettingsController() { private val db: DatabaseHelper = Injekt.get() override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_library + titleRes = R.string.library preferenceCategory { - titleRes = R.string.pref_category_library_display - preference { - titleRes = R.string.pref_library_columns - onClick { - LibraryColumnsDialog().showDialog(router) - } - - fun getColumnValue(value: Int): String { - return if (value == 0) context.getString(R.string.default_columns) - else value.toString() - } - - Observable.combineLatest(preferences.portraitColumns().asObservable(), - preferences.landscapeColumns().asObservable(), - { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) }) - .subscribeUntilDestroy { (portraitCols, landscapeCols) -> - val portrait = getColumnValue(portraitCols) - val landscape = getColumnValue(landscapeCols) - summary = - "${context.getString(R.string.portrait)}: $portrait, " + "${context.getString( - R.string.landscape - )}: $landscape" - } - } - + titleRes = R.string.display switchPreference { key = Keys.removeArticles - titleRes = R.string.pref_remove_articles - summaryRes = R.string.pref_remove_articles_summary + titleRes = R.string.sort_by_ignoring_articles + summaryRes = R.string.when_sorting_ignore_articles defaultValue = false } } @@ -63,19 +32,59 @@ class SettingsLibraryController : SettingsController() { val dbCategories = db.getCategories().executeAsBlocking() preferenceCategory { - titleRes = R.string.pref_category_library_update + titleRes = R.string.categories + preference { + titleRes = R.string.edit_categories + val catCount = db.getCategories().executeAsBlocking().size + summary = context.resources.getQuantityString(R.plurals.category, catCount, catCount) + onClick { router.pushController(CategoryController().withFadeTransaction()) } + } + intListPreference(activity) { + key = Keys.defaultCategory + titleRes = R.string.default_category + + val categories = listOf(Category.createDefault(context)) + dbCategories + entries = + listOf(context.getString(R.string.always_ask)) + categories.map { it.name }.toTypedArray() + entryValues = listOf(-1) + categories.mapNotNull { it.id }.toList() + defaultValue = "-1" + + val selectedCategory = categories.find { it.id == preferences.defaultCategory() } + summary = + selectedCategory?.name ?: context.getString(R.string.always_ask) + onChange { newValue -> + summary = categories.find { + it.id == newValue as Int + }?.name ?: context.getString(R.string.always_ask) + true + } + } + } + + preferenceCategory { + titleRes = R.string.updates + intListPreference(activity) { + key = Keys.updateOnRefresh + titleRes = R.string.categories_on_manual + + entriesRes = arrayOf( + R.string.first_category, R.string.categories_in_global_update + ) + entryRange = 0..1 + defaultValue = -1 + } intListPreference(activity) { key = Keys.libraryUpdateInterval - titleRes = R.string.pref_library_update_interval + titleRes = R.string.library_update_frequency entriesRes = arrayOf( - R.string.update_never, - R.string.update_1hour, - R.string.update_2hour, - R.string.update_3hour, - R.string.update_6hour, - R.string.update_12hour, - R.string.update_24hour, - R.string.update_48hour + R.string.manual, + R.string.hourly, + R.string.every_2_hours, + R.string.every_3_hours, + R.string.every_6_hours, + R.string.every_12_hours, + R.string.daily, + R.string.every_2_days ) entryValues = listOf(0, 1, 2, 3, 6, 12, 24, 48) defaultValue = 0 @@ -93,10 +102,10 @@ class SettingsLibraryController : SettingsController() { } multiSelectListPreferenceMat(activity) { key = Keys.libraryUpdateRestriction - titleRes = R.string.pref_library_update_restriction + titleRes = R.string.library_update_restriction entriesRes = arrayOf(R.string.wifi, R.string.charging) entryValues = listOf("wifi", "ac") - customSummaryRes = R.string.pref_library_update_restriction_summary + customSummaryRes = R.string.library_update_restriction_summary preferences.libraryUpdateInterval().asObservable() .subscribeUntilDestroy { isVisible = it > 0 } @@ -109,34 +118,34 @@ class SettingsLibraryController : SettingsController() { } switchPreference { key = Keys.updateOnlyNonCompleted - titleRes = R.string.pref_update_only_non_completed + titleRes = R.string.only_update_ongoing defaultValue = false } intListPreference(activity) { key = Keys.libraryUpdatePrioritization - titleRes = R.string.pref_library_update_prioritization + titleRes = R.string.library_update_order // The following array lines up with the list rankingScheme in: // ../../data/library/LibraryUpdateRanker.kt entriesRes = arrayOf( - R.string.action_sort_alpha, R.string.action_sort_last_updated + R.string.alphabetically, R.string.last_updated ) entryRange = 0..1 defaultValue = 0 - summaryRes = R.string.pref_library_update_prioritization_summary + summaryRes = R.string.select_order_to_update } switchPreference { key = Keys.refreshCoversToo - titleRes = R.string.pref_refresh_covers_too - summaryRes = R.string.pref_refresh_covers_too_summary + titleRes = R.string.auto_refresh_covers + summaryRes = R.string.auto_refresh_covers_summary defaultValue = true } multiSelectListPreferenceMat(activity) { key = Keys.libraryUpdateCategories - titleRes = R.string.pref_library_update_categories + titleRes = R.string.categories_to_include_in_global_update entries = dbCategories.map { it.name } entryValues = dbCategories.map { it.id.toString() } allSelectionRes = R.string.all @@ -152,86 +161,18 @@ class SettingsLibraryController : SettingsController() { } } } - preferenceCategory { - titleRes = R.string.pref_category_library_categories - intListPreference(activity) { - key = Keys.defaultCategory - titleRes = R.string.default_category - - val categories = listOf(Category.createDefault(context)) + dbCategories - entries = - listOf(context.getString(R.string.default_category_summary)) + categories.map { it.name }.toTypedArray() - entryValues = listOf(-1) + categories.mapNotNull { it.id }.toList() - defaultValue = "-1" - - val selectedCategory = categories.find { it.id == preferences.defaultCategory() } - summary = - selectedCategory?.name ?: context.getString(R.string.default_category_summary) - onChange { newValue -> - summary = categories.find { - it.id == newValue as Int - }?.name ?: context.getString(R.string.default_category_summary) - true - } - } - } if (preferences.skipPreMigration().getOrDefault() || preferences.migrationSources().getOrDefault().isNotEmpty()) { preferenceCategory { - titleRes = R.string.pref_category_library_migration + titleRes = R.string.migration // Only show this if someone has mass migrated manga once switchPreference { key = Keys.skipPreMigration - titleRes = R.string.pref_skip_pre_migration - summaryRes = R.string.pref_skip_pre_migration_summary + titleRes = R.string.skip_pre_migration + summaryRes = R.string.use_last_saved_migration_preferences defaultValue = false } } } } - - class LibraryColumnsDialog : DialogController() { - - private val preferences: PreferencesHelper = Injekt.get() - - private var portrait = preferences.portraitColumns().getOrDefault() - private var landscape = preferences.landscapeColumns().getOrDefault() - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val dialog = MaterialDialog(activity!!) - .title(R.string.pref_library_columns) - .customView(viewRes = R.layout.pref_library_columns, scrollable = false) - .positiveButton(android.R.string.ok) { - preferences.portraitColumns().set(portrait) - preferences.landscapeColumns().set(landscape) - } - .negativeButton(android.R.string.cancel) - - onViewCreated(dialog.view) - return dialog - } - - fun onViewCreated(view: View) { - with(view.portrait_columns) { - displayedValues = arrayOf(context.getString(R.string.default_columns)) + - IntRange(1, 10).map(Int::toString) - value = portrait - - setOnValueChangedListener { _, _, newValue -> - portrait = newValue - } - } - with(view.landscape_columns) { - displayedValues = arrayOf(context.getString(R.string.default_columns)) + - IntRange(1, 10).map(Int::toString) - value = landscape - - setOnValueChangedListener { _, _, newValue -> - landscape = newValue - } - } - } - - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 2ac8ea26ca..0d7e03b020 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -1,46 +1,62 @@ package eu.kanade.tachiyomi.ui.setting +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import androidx.preference.PreferenceScreen +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferenceKeys -import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.migration.MigrationController import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.openInBrowser class SettingsMainController : SettingsController() { + + init { + setHasOptionsMenu(true) + } + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.label_settings + titleRes = R.string.settings val tintColor = context.getResourceColor(R.attr.colorAccent) preference { - iconRes = R.drawable.ic_tune_black_24dp + iconRes = R.drawable.ic_tune_white_24dp iconTint = tintColor - titleRes = R.string.pref_category_general + titleRes = R.string.general onClick { navigateTo(SettingsGeneralController()) } } preference { iconRes = R.drawable.ic_book_black_24dp iconTint = tintColor - titleRes = R.string.pref_category_library + titleRes = R.string.library onClick { navigateTo(SettingsLibraryController()) } } preference { - iconRes = R.drawable.ic_chrome_reader_mode_black_24dp + iconRes = R.drawable.ic_read_24dp iconTint = tintColor - titleRes = R.string.pref_category_reader + titleRes = R.string.reader onClick { navigateTo(SettingsReaderController()) } } preference { iconRes = R.drawable.ic_file_download_black_24dp iconTint = tintColor - titleRes = R.string.pref_category_downloads + titleRes = R.string.downloads onClick { navigateTo(SettingsDownloadController()) } } + preference { + iconRes = R.drawable.ic_swap_calls_white_24dp + iconTint = tintColor + titleRes = R.string.source_migration + onClick { navigateTo(MigrationController()) } + } preference { iconRes = R.drawable.ic_sync_black_24dp iconTint = tintColor - titleRes = R.string.pref_category_tracking + titleRes = R.string.tracking onClick { navigateTo(SettingsTrackingController()) } } preference { @@ -52,18 +68,36 @@ class SettingsMainController : SettingsController() { preference { iconRes = R.drawable.ic_code_black_24dp iconTint = tintColor - titleRes = R.string.pref_category_advanced + titleRes = R.string.advanced onClick { navigateTo(SettingsAdvancedController()) } } preference { - iconRes = R.drawable.ic_help_black_24dp + iconRes = R.drawable.ic_info_black_24dp iconTint = tintColor - titleRes = R.string.pref_category_about + titleRes = R.string.about onClick { navigateTo(SettingsAboutController()) } } } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.settings_main, menu) + menu.findItem(R.id.action_bug_report).isVisible = BuildConfig.DEBUG + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_help -> activity?.openInBrowser(URL_HELP) + R.id.action_bug_report -> activity?.openInBrowser(URL_BUG_REPORT) + else -> return super.onOptionsItemSelected(item) + } + return true + } - private fun navigateTo(controller: SettingsController) { + private fun navigateTo(controller: Controller) { router.pushController(controller.withFadeTransaction()) } + + private companion object { + private const val URL_HELP = "https://tachiyomi.org/help/" + private const val URL_BUG_REPORT = "https://github.com/Jays2Kings/tachiyomiJ2K/issues" + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 0e23b82aae..416f7a75d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -8,133 +8,137 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsReaderController : SettingsController() { override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_reader + titleRes = R.string.reader intListPreference(activity) { key = Keys.defaultViewer - titleRes = R.string.pref_viewer_type + titleRes = R.string.default_viewer entriesRes = arrayOf(R.string.left_to_right_viewer, R.string.right_to_left_viewer, - R.string.vertical_viewer, R.string.webtoon_viewer) + R.string.vertical_viewer, R.string.webtoon) entryRange = 1..4 defaultValue = 1 } intListPreference(activity) { key = Keys.imageScaleType - titleRes = R.string.pref_image_scale_type - entriesRes = arrayOf(R.string.scale_type_fit_screen, R.string.scale_type_stretch, - R.string.scale_type_fit_width, R.string.scale_type_fit_height, - R.string.scale_type_original_size, R.string.scale_type_smart_fit) + titleRes = R.string.scale_type + entriesRes = arrayOf(R.string.fit_screen, R.string.stretch, + R.string.fit_width, R.string.fit_height, + R.string.original_size, R.string.smart_fit) entryRange = 1..6 defaultValue = 1 } intListPreference(activity) { key = Keys.zoomStart - titleRes = R.string.pref_zoom_start - entriesRes = arrayOf(R.string.zoom_start_automatic, R.string.zoom_start_left, - R.string.zoom_start_right, R.string.zoom_start_center) + titleRes = R.string.zoom_start_position + entriesRes = arrayOf(R.string.automatic, R.string.left, + R.string.right, R.string.center) entryRange = 1..4 defaultValue = 1 } intListPreference(activity) { key = Keys.rotation - titleRes = R.string.pref_rotation_type - entriesRes = arrayOf(R.string.rotation_free, R.string.rotation_lock, - R.string.rotation_force_portrait, R.string.rotation_force_landscape) + titleRes = R.string.rotation + entriesRes = arrayOf(R.string.free, R.string.lock, + R.string.force_portrait, R.string.force_landscape) entryRange = 1..4 defaultValue = 1 } intListPreference(activity) { key = Keys.readerTheme - titleRes = R.string.pref_reader_theme - entriesRes = arrayOf(R.string.white_background, R.string.black_background, R.string - .reader_theme_smart, R.string.reader_theme_smart_theme) + titleRes = R.string.background_color + entriesRes = arrayOf(R.string.white, R.string.black, R.string + .smart_based_on_page, R.string.smart_based_on_page_and_theme) entryRange = 0..3 defaultValue = 2 } intListPreference(activity) { key = Keys.doubleTapAnimationSpeed - titleRes = R.string.pref_double_tap_anim_speed - entries = listOf(context.getString(R.string.double_tap_anim_speed_0), context.getString(R - .string.double_tap_anim_speed_fast), context.getString(R.string.double_tap_anim_speed_normal)) + titleRes = R.string.double_tap_anim_speed + entries = listOf(context.getString(R.string.no_animation), context.getString(R + .string.fast), context.getString(R.string.normal)) entryValues = listOf(1, 250, 500) // using a value of 0 breaks the image viewer, so // min is 1 defaultValue = 500 } switchPreference { key = Keys.skipRead - titleRes = R.string.pref_skip_read_chapters + titleRes = R.string.skip_read_chapters defaultValue = false } switchPreference { key = Keys.fullscreen - titleRes = R.string.pref_fullscreen + titleRes = R.string.fullscreen defaultValue = true } switchPreference { key = Keys.keepScreenOn - titleRes = R.string.pref_keep_screen_on + titleRes = R.string.keep_screen_on defaultValue = true } switchPreference { key = Keys.showPageNumber - titleRes = R.string.pref_show_page_number + titleRes = R.string.show_page_number defaultValue = true } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { switchPreference { key = Keys.trueColor - titleRes = R.string.pref_true_color + titleRes = R.string.true_32bit_color defaultValue = false } } + switchPreference { + key = Keys.alwaysShowChapterTransition + titleRes = R.string.always_show_chapter_transition + defaultValue = true + } preferenceCategory { titleRes = R.string.pager_viewer switchPreference { key = Keys.enableTransitions - titleRes = R.string.pref_page_transitions + titleRes = R.string.page_transitions defaultValue = true } switchPreference { key = Keys.cropBorders - titleRes = R.string.pref_crop_borders + titleRes = R.string.crop_borders defaultValue = false } } preferenceCategory { - titleRes = R.string.webtoon_viewer + titleRes = R.string.webtoon switchPreference { key = Keys.cropBordersWebtoon - titleRes = R.string.pref_crop_borders + titleRes = R.string.crop_borders defaultValue = false } } preferenceCategory { - titleRes = R.string.pref_reader_navigation + titleRes = R.string.navigation switchPreference { key = Keys.readWithTapping - titleRes = R.string.pref_read_with_tapping + titleRes = R.string.tapping defaultValue = true } switchPreference { key = Keys.readWithLongTap - titleRes = R.string.pref_read_with_long_tap + titleRes = R.string.long_tap_dialog defaultValue = true } switchPreference { key = Keys.readWithVolumeKeys - titleRes = R.string.pref_read_with_volume_keys + titleRes = R.string.volume_keys defaultValue = false } switchPreference { key = Keys.readWithVolumeKeysInverted - titleRes = R.string.pref_read_with_volume_keys_inverted + titleRes = R.string.invert_volume_keys defaultValue = false }.apply { dependency = Keys.readWithVolumeKeys } } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt index dc12f13669..40ecc4186a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt @@ -35,11 +35,11 @@ class SettingsSourcesController : SettingsController(), private var orderedLangs = listOf() private var langPrefs = mutableListOf>() - private var sourcesByLang:TreeMap> = TreeMap() + private var sourcesByLang: TreeMap> = TreeMap() private var sorting = SourcesSort.Alpha override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_sources + titleRes = R.string.filter sorting = SourcesSort.from(preferences.sourceSorting().getOrDefault()) ?: SourcesSort.Alpha activity?.invalidateOptionsMenu() // Get the list of active language codes. @@ -93,7 +93,7 @@ class SettingsSourcesController : SettingsController(), val selectAllPreference = CheckBoxPreference(group.context).apply { - title = "\t\t${context.getString(R.string.pref_category_all_sources)}" + title = "\t\t${context.getString(R.string.all_sources)}" key = "all_${sources.first().lang}" isPersistent = false isChecked = sources.all { it.id.toString() !in hiddenCatalogues } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt index e213d685ea..dbd5ea3e93 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.anilist.AnilistApi -import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi +import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog @@ -22,11 +22,11 @@ class SettingsTrackingController : SettingsController(), private val trackManager: TrackManager by injectLazy() override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { - titleRes = R.string.pref_category_tracking + titleRes = R.string.tracking switchPreference { key = Keys.autoUpdateTrack - titleRes = R.string.pref_auto_update_manga_sync + titleRes = R.string.sync_chapters_after_reading defaultValue = true } preferenceCategory { @@ -42,7 +42,7 @@ class SettingsTrackingController : SettingsController(), trackPreference(trackManager.aniList) { onClick { val tabsIntent = CustomTabsIntent.Builder() - .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) + .setToolbarColor(context.getResourceColor(R.attr.colorPrimaryVariant)) .build() tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) tabsIntent.launchUrl(activity!!, AnilistApi.authUrl()) @@ -50,7 +50,7 @@ class SettingsTrackingController : SettingsController(), } trackPreference(trackManager.kitsu) { onClick { - val dialog = TrackLoginDialog(trackManager.kitsu) + val dialog = TrackLoginDialog(trackManager.kitsu, context.getString(R.string.email)) dialog.targetController = this@SettingsTrackingController dialog.showDialog(router) } @@ -58,7 +58,7 @@ class SettingsTrackingController : SettingsController(), trackPreference(trackManager.shikimori) { onClick { val tabsIntent = CustomTabsIntent.Builder() - .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) + .setToolbarColor(context.getResourceColor(R.attr.colorPrimaryVariant)) .build() tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) tabsIntent.launchUrl(activity!!, ShikimoriApi.authUrl()) @@ -67,7 +67,7 @@ class SettingsTrackingController : SettingsController(), trackPreference(trackManager.bangumi) { onClick { val tabsIntent = CustomTabsIntent.Builder() - .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) + .setToolbarColor(context.getResourceColor(R.attr.colorPrimaryVariant)) .build() tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) tabsIntent.launchUrl(activity!!, BangumiApi.authUrl()) @@ -77,8 +77,8 @@ class SettingsTrackingController : SettingsController(), } inline fun PreferenceScreen.trackPreference( - service: TrackService, - block: (@DSL LoginPreference).() -> Unit + service: TrackService, + block: (@DSL LoginPreference).() -> Unit ): LoginPreference { return initThenAdd(LoginPreference(context).apply { key = Keys.trackUsername(service.id) @@ -101,5 +101,4 @@ class SettingsTrackingController : SettingsController(), override fun trackDialogClosed(service: TrackService) { updatePreference(service.id) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt index 0418fdaa63..cdee2846a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/AnilistLoginActivity.kt @@ -2,21 +2,26 @@ package eu.kanade.tachiyomi.ui.setting.track import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity import android.view.Gravity.CENTER import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout import android.widget.ProgressBar +import androidx.appcompat.app.AppCompatActivity import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.ui.main.MainActivity -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy class AnilistLoginActivity : AppCompatActivity() { private val trackManager: TrackManager by injectLazy() + private val scope = CoroutineScope(Job() + Dispatchers.Main) + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -26,20 +31,21 @@ class AnilistLoginActivity : AppCompatActivity() { val regex = "(?:access_token=)(.*?)(?:&)".toRegex() val matchResult = regex.find(intent.data?.fragment.toString()) if (matchResult?.groups?.get(1) != null) { - trackManager.aniList.login(matchResult.groups[1]!!.value) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - returnToSettings() - }, { - returnToSettings() - }) + scope.launch { + trackManager.aniList.login(matchResult.groups[1]!!.value) + returnToSettings() + } } else { trackManager.aniList.logout() returnToSettings() } } + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + private fun returnToSettings() { finish() @@ -47,5 +53,4 @@ class AnilistLoginActivity : AppCompatActivity() { intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt index bac59b4f6a..2987245515 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BangumiLoginActivity.kt @@ -2,21 +2,25 @@ package eu.kanade.tachiyomi.ui.setting.track import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity import android.view.Gravity.CENTER import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout import android.widget.ProgressBar +import androidx.appcompat.app.AppCompatActivity import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.ui.main.MainActivity -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy class BangumiLoginActivity : AppCompatActivity() { private val trackManager: TrackManager by injectLazy() + private val scope = CoroutineScope(Job() + Dispatchers.Main) + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -25,14 +29,10 @@ class BangumiLoginActivity : AppCompatActivity() { val code = intent.data?.getQueryParameter("code") if (code != null) { - trackManager.bangumi.login(code) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - returnToSettings() - }, { - returnToSettings() - }) + scope.launch { + trackManager.bangumi.login(code) + returnToSettings() + } } else { trackManager.bangumi.logout() returnToSettings() @@ -46,5 +46,4 @@ class BangumiLoginActivity : AppCompatActivity() { intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikomoriLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikimoriLoginActivity.kt similarity index 74% rename from app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikomoriLoginActivity.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikimoriLoginActivity.kt index 682f998606..25fed5e2d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikomoriLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/ShikimoriLoginActivity.kt @@ -2,21 +2,25 @@ package eu.kanade.tachiyomi.ui.setting.track import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity import android.view.Gravity.CENTER import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout import android.widget.ProgressBar +import androidx.appcompat.app.AppCompatActivity import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.ui.main.MainActivity -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy class ShikimoriLoginActivity : AppCompatActivity() { private val trackManager: TrackManager by injectLazy() + private val scope = CoroutineScope(Job() + Dispatchers.Main) + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -25,14 +29,10 @@ class ShikimoriLoginActivity : AppCompatActivity() { val code = intent.data?.getQueryParameter("code") if (code != null) { - trackManager.shikimori.login(code) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - returnToSettings() - }, { - returnToSettings() - }) + scope.launch { + trackManager.shikimori.login(code) + returnToSettings() + } } else { trackManager.shikimori.logout() returnToSettings() @@ -46,5 +46,4 @@ class ShikimoriLoginActivity : AppCompatActivity() { intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 78050d4bee..91fd2df6a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -7,6 +7,7 @@ import android.graphics.Bitmap import android.graphics.Color import android.os.Build import android.os.Bundle +import android.util.TypedValue import android.view.Menu import android.view.MenuItem import android.view.View @@ -14,6 +15,7 @@ import android.view.ViewGroup import android.webkit.WebChromeClient import android.webkit.WebView import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatDelegate import androidx.core.graphics.ColorUtils import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.SourceManager @@ -23,26 +25,27 @@ import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets import eu.kanade.tachiyomi.util.view.invisible import eu.kanade.tachiyomi.util.view.marginBottom +import eu.kanade.tachiyomi.util.view.setStyle import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePadding import eu.kanade.tachiyomi.util.view.visible import kotlinx.android.synthetic.main.webview_activity.* +import kotlinx.android.synthetic.main.webview_activity.swipe_refresh import uy.kohesive.injekt.injectLazy class WebViewActivity : BaseActivity() { private val sourceManager by injectLazy() - private var bundle:Bundle? = null + private var bundle: Bundle? = null companion object { const val SOURCE_KEY = "source_key" const val URL_KEY = "url_key" const val TITLE_KEY = "title_key" - fun newIntent(context: Context, sourceId: Long, url: String, title:String?): Intent { + fun newIntent(context: Context, sourceId: Long, url: String, title: String?): Intent { val intent = Intent(context, WebViewActivity::class.java) intent.putExtra(SOURCE_KEY, sourceId) intent.putExtra(URL_KEY, url) @@ -52,18 +55,13 @@ class WebViewActivity : BaseActivity() { } } - /*override fun getTheme(): Resources.Theme { - val theme = super.getTheme() - theme.applyStyle(when (preferences.theme()) { - 3, 6 -> R.style.Theme_Tachiyomi_Amoled - 4, 7 -> R.style.Theme_Tachiyomi_DarkBlue - else -> R.style.Theme_Tachiyomi - }, true) - return theme - }*/ - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + delegate.localNightMode = when (preferences.theme()) { + 1, 8 -> AppCompatDelegate.MODE_NIGHT_NO + 2, 3, 4 -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } setContentView(R.layout.webview_activity) title = intent.extras?.getString(TITLE_KEY) setSupportActionBar(toolbar) @@ -71,15 +69,12 @@ class WebViewActivity : BaseActivity() { toolbar.setNavigationOnClickListener { super.onBackPressed() } + toolbar.navigationIcon?.setTint(getResourceColor(R.attr.actionBarTintColor)) - val container:ViewGroup = findViewById(R.id.web_view_layout) + val container: ViewGroup = findViewById(R.id.web_view_layout) val content: LinearLayout = findViewById(R.id.web_linear_layout) - container.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + container.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION container.setOnApplyWindowInsetsListener { v, insets -> val contextView = window?.decorView?.findViewById(R.id.action_mode_bar) @@ -98,24 +93,31 @@ class WebViewActivity : BaseActivity() { 0, insets.systemWindowInsetBottom ) } + swipe_refresh.setStyle() swipe_refresh.setOnRefreshListener { refreshPage() } + window.statusBarColor = ColorUtils.setAlphaComponent(getResourceColor(R.attr + .colorSecondary), 255) + content.setOnApplyWindowInsetsListener { v, insets -> - window.statusBarColor = getResourceColor(R.attr.colorPrimary) - window.navigationBarColor = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - v.context.getResourceColor(android.R.attr.colorPrimary) - } - // if the android q+ device has gesture nav, transparent nav bar - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && (v.rootWindowInsets.systemWindowInsetBottom != v.rootWindowInsets - .tappableElementInsets.bottom)) { - getColor(android.R.color.transparent) - } else { - v.context.getResourceColor(android.R.attr.colorBackground) - } + // if pure white theme on a device that does not support dark status bar + /*if (getResourceColor(android.R.attr.statusBarColor) != Color.TRANSPARENT) + window.statusBarColor = Color.BLACK + else window.statusBarColor = getResourceColor(R.attr.colorPrimary)*/ + window.navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val colorPrimary = getResourceColor(R.attr.colorPrimaryVariant) + if (colorPrimary == Color.WHITE) Color.BLACK + else getResourceColor(android.R.attr.colorPrimary) + } + // if the android q+ device has gesture nav, transparent nav bar + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + (v.rootWindowInsets.systemWindowInsetBottom != v.rootWindowInsets.tappableElementInsets.bottom)) { + getColor(android.R.color.transparent) + } else { + getResourceColor(android.R.attr.colorBackground) + } v.setPadding(insets.systemWindowInsetLeft, insets.systemWindowInsetTop, insets.systemWindowInsetRight, 0) val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -162,23 +164,23 @@ class WebViewActivity : BaseActivity() { override fun onPageCommitVisible(view: WebView?, url: String?) { super.onPageCommitVisible(view, url) - nested_view.scrollTo(0,0) + nested_view.scrollTo(0, 0) } } val marginB = webview.marginBottom - webview.doOnApplyWindowInsets { v, insets, _ -> + webview.setOnApplyWindowInsetsListener { v, insets -> val bottomInset = if (Build.VERSION.SDK_INT >= 29) insets.tappableElementInsets.bottom else insets.systemWindowInsetBottom v.updateLayoutParams { bottomMargin = marginB + bottomInset } + insets } webview.settings.javaScriptEnabled = true webview.settings.userAgentString = source.headers["User-Agent"] webview.loadUrl(url, headers) - } - else { + } else { webview.restoreState(bundle) } } @@ -189,23 +191,41 @@ class WebViewActivity : BaseActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - window.statusBarColor = getResourceColor(R.attr.colorPrimary) - toolbar.setBackgroundColor(getResourceColor(R.attr.colorPrimary)) + val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + val lightMode = currentNightMode == Configuration.UI_MODE_NIGHT_NO + window.statusBarColor = ColorUtils.setAlphaComponent(getResourceColor(R.attr + .colorSecondary), 255) + toolbar.setBackgroundColor(getResourceColor(R.attr.colorSecondary)) + toolbar.popupTheme = if (lightMode) R.style.ThemeOverlay_MaterialComponents else R + .style.ThemeOverlay_MaterialComponents_Dark + val tintColor = getResourceColor(R.attr.actionBarTintColor) + toolbar.navigationIcon?.setTint(tintColor) + toolbar.overflowIcon?.mutate() + toolbar.setTitleTextColor(tintColor) + toolbar.overflowIcon?.setTint(tintColor) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - window.navigationBarColor = getResourceColor(android.R.attr.colorPrimary) + window.navigationBarColor = getResourceColor(R.attr.colorPrimaryVariant) else if (window.navigationBarColor != getColor(android.R.color.transparent)) window.navigationBarColor = getResourceColor(android.R.attr.colorBackground) - val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK - if (Build.VERSION.SDK_INT >= 26) { - if (currentNightMode == Configuration.UI_MODE_NIGHT_NO) { - web_linear_layout.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - } else { - web_linear_layout.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + web_linear_layout.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && lightMode) { + web_linear_layout.systemUiVisibility = web_linear_layout.systemUiVisibility.or(View + .SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) } + val typedValue = TypedValue() + theme.resolveAttribute(android.R.attr.windowLightStatusBar, typedValue, true) + + if (typedValue.data == -1) + web_linear_layout.systemUiVisibility = web_linear_layout.systemUiVisibility + .or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) + else + web_linear_layout.systemUiVisibility = web_linear_layout.systemUiVisibility + .rem(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) + invalidateOptionsMenu() } /** @@ -224,9 +244,10 @@ class WebViewActivity : BaseActivity() { val hasHistory = webview.canGoBack() || webview.canGoForward() backItem?.isVisible = hasHistory forwardItem?.isVisible = hasHistory - val translucentWhite = ColorUtils.setAlphaComponent(Color.WHITE, 127) - backItem.icon?.setTint(if (webview.canGoBack()) Color.WHITE else translucentWhite) - forwardItem?.icon?.setTint(if (webview.canGoForward()) Color.WHITE else translucentWhite) + val tintColor = getResourceColor(R.attr.actionBarTintColor) + val translucentWhite = ColorUtils.setAlphaComponent(tintColor, 127) + backItem.icon?.setTint(if (webview.canGoBack()) tintColor else translucentWhite) + forwardItem?.icon?.setTint(if (webview.canGoForward()) tintColor else translucentWhite) return super.onPrepareOptionsMenu(menu) } @@ -242,7 +263,7 @@ class WebViewActivity : BaseActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_web_back -> webview.goBack() - R.id.action_web_forward -> webview.goForward() + R.id.action_web_forward -> webview.goForward() R.id.action_web_share -> shareWebpage() R.id.action_web_browser -> openInBrowser() } @@ -255,7 +276,7 @@ class WebViewActivity : BaseActivity() { type = "text/plain" putExtra(Intent.EXTRA_TEXT, webview.url) } - startActivity(Intent.createChooser(intent, getString(R.string.action_share))) + startActivity(Intent.createChooser(intent, getString(R.string.share))) } catch (e: Exception) { toast(e.message) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt index 3601905556..d0da5f6fb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt @@ -74,7 +74,7 @@ object ChapterRecognition { } // Remove manga title from chapter title. - val nameWithoutManga = name.replace(manga.originalTitle().toLowerCase(), "").trim() + val nameWithoutManga = name.replace(manga.title.toLowerCase(), "").trim() // Check if first value is number after title remove. if (updateChapter(withoutManga.find(nameWithoutManga), chapter)) @@ -140,5 +140,4 @@ object ChapterRecognition { private fun parseAlphaPostFix(alpha: Char): Float { return ("0." + (alpha.toInt() - 96).toString()).toFloat() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 3f88cc7160..168dc94e03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -6,7 +6,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.HttpSource -import java.util.* +import java.util.Date +import java.util.TreeSet /** * Helper method for syncing the list of chapters from the source with the ones from the database. @@ -17,10 +18,12 @@ import java.util.* * @param source the source of the chapters. * @return a pair of new insertions and deletions. */ -fun syncChaptersWithSource(db: DatabaseHelper, - rawSourceChapters: List, - manga: Manga, - source: Source): Pair, List> { +fun syncChaptersWithSource( + db: DatabaseHelper, + rawSourceChapters: List, + manga: Manga, + source: Source +): Pair, List> { if (rawSourceChapters.isEmpty()) { throw Exception("No chapters found") @@ -50,7 +53,7 @@ fun syncChaptersWithSource(db: DatabaseHelper, if (dbChapter == null) { toAdd.add(sourceChapter) } else { - //this forces metadata update for the main viewable things in the chapter list + // this forces metadata update for the main viewable things in the chapter list if (source is HttpSource) { source.prepareNewChapter(sourceChapter, manga) } @@ -139,15 +142,14 @@ fun syncChaptersWithSource(db: DatabaseHelper, if (dateFetch == 0L) { if (toAdd.isNotEmpty()) manga.last_update = Date().time - } - else manga.last_update = dateFetch + } else manga.last_update = dateFetch db.updateLastUpdated(manga).executeAsBlocking() } return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList()) } -//checks if the chapter in db needs updated +// checks if the chapter in db needs updated private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: SChapter): Boolean { return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name || dbChapter.date_upload != sourceChapter.date_upload || diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/RetryWithDelay.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/RetryWithDelay.kt index 7cdef09b13..bc1d9a801a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/RetryWithDelay.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/RetryWithDelay.kt @@ -7,9 +7,9 @@ import rx.schedulers.Schedulers import java.util.concurrent.TimeUnit.MILLISECONDS class RetryWithDelay( - private val maxRetries: Int = 1, - private val retryStrategy: (Int) -> Int = { 1000 }, - private val scheduler: Scheduler = Schedulers.computation() + private val maxRetries: Int = 1, + private val retryStrategy: (Int) -> Int = { 1000 }, + private val scheduler: Scheduler = Schedulers.computation() ) : Func1, Observable<*>> { private var retryCount = 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index d9f1772691..4ad9f57b4c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -114,4 +114,3 @@ object DiskUtil { } } } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt index 4727e56351..a6442e6faa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -113,5 +113,4 @@ class EpubFile(file: File) : Closeable { } } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt index 2b2ad5843c..53523c6d4c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt @@ -17,4 +17,3 @@ fun File.getUriCompat(context: Context): Uri { FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this) else Uri.fromFile(this) } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt index 85bd0c3245..c3f72c8662 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.util.storage import okio.BufferedSource -import okio.Okio import okio.buffer import okio.sink import java.io.File diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index e14d200d54..727f0d6f06 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -12,13 +12,12 @@ import android.content.res.Resources import android.net.ConnectivityManager import android.net.Uri import android.os.PowerManager +import android.widget.Toast import androidx.annotation.AttrRes import androidx.annotation.StringRes import androidx.browser.customtabs.CustomTabsIntent import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import android.widget.Toast import com.nononsenseapps.filepicker.FilePickerActivity import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity @@ -75,8 +74,8 @@ fun Context.getFilePicker(currentDir: String): Intent { * @param permission the permission to check. * @return true if it has permissions. */ -fun Context.hasPermission(permission: String) - = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED +fun Context.hasPermission(permission: String) = + ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED /** * Returns the color for the given attribute. @@ -102,6 +101,9 @@ val Int.pxToDp: Int val Int.dpToPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() +val Float.dpToPx: Float + get() = (this * Resources.getSystem().displayMetrics.density) + /** * Property to get the notification manager from the context. */ @@ -174,7 +176,7 @@ fun Context.openInBrowser(url: String) { try { val parsedUrl = Uri.parse(url) val intent = CustomTabsIntent.Builder() - .setToolbarColor(getResourceColor(R.attr.colorPrimary)) + .setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant)) .build() intent.launchUrl(this, parsedUrl) } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt index 2753390544..b3ef44617f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt @@ -2,11 +2,10 @@ package eu.kanade.tachiyomi.util.system import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlin.coroutines.EmptyCoroutineContext fun launchUI(block: suspend CoroutineScope.() -> Unit): Job = GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DatabaseExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DatabaseExtensions.kt new file mode 100644 index 0000000000..94c6fdf3b5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DatabaseExtensions.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.util.system + +import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects +import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject +import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects +import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun PreparedGetListOfObjects.executeOnIO(): List { + return withContext(Dispatchers.IO) { executeAsBlocking() } +} + +suspend fun PreparedGetObject.executeOnIO(): T? { + return withContext(Dispatchers.IO) { executeAsBlocking() } +} + +suspend fun PreparedPutObject.executeOnIO() { + withContext(Dispatchers.IO) { executeAsBlocking() } +} + +suspend fun PreparedPutCollectionOfObjects.executeOnIO() { + withContext(Dispatchers.IO) { executeAsBlocking() } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index 0f1ca55bf6..44e4a98f04 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.util.system import android.graphics.Bitmap import android.graphics.Color -import android.graphics.drawable.* +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable import java.io.InputStream import java.net.URLConnection import kotlin.math.abs @@ -48,13 +50,14 @@ object ImageUtil { if (bytes.compareWith("RIFF".toByteArray())) { return ImageType.WEBP } - } catch(e: Exception) { + } catch (e: Exception) { } return null } - fun autoSetBackground(image: Bitmap, useWhiteAlways: Boolean): Drawable { + fun autoSetBackground(image: Bitmap?, useWhiteAlways: Boolean): Drawable { val backgroundColor = if (useWhiteAlways) Color.WHITE else android.R.attr.colorBackground + if (image == null) return ColorDrawable(backgroundColor) if (image.width < 50 || image.height < 50) return ColorDrawable(backgroundColor) val top = 5 @@ -73,8 +76,8 @@ object ImageUtil { val botLeftIsDark = isDark(image.getPixel(left, bot)) val botRightIsDark = isDark(image.getPixel(right, bot)) - var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) - || (topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark)) + var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) || + (topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark)) if (!isWhite(image.getPixel(left, top)) && pixelIsClose(image.getPixel(left, top), image.getPixel(midX, top)) && !isWhite(image.getPixel(midX, top)) && pixelIsClose(image.getPixel(midX, top), image.getPixel(right, top)) && @@ -114,7 +117,7 @@ object ImageUtil { val notOffset = x == left || x == right for ((index, y) in (0 until image.height step image.height / 25).withIndex()) { val pixel = image.getPixel(x, y) - val pixelOff = image.getPixel(x + (if (x < image.width/2) -offsetX else offsetX), y) + val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y) if (isWhite(pixel)) { whitePixelsStreak++ whitePixels++ @@ -139,7 +142,6 @@ object ImageUtil { if (blackPixelsStreak > 6 && blackPixelsStreak >= index - 1) topBlackStreak = blackPixelsStreak blackPixelsStreak = 0 - } if (blackPixelsStreak > 6) botBlackStreak = blackPixelsStreak @@ -187,14 +189,14 @@ object ImageUtil { intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel)) else ColorDrawable(blackPixel) } - if (topIsBlackStreak || (topLeftIsDark && topRightIsDark - && isDark(image.getPixel(left - offsetX, top)) && isDark(image.getPixel(right + offsetX, top)) - && (topMidIsDark || overallBlackPixels > 9))) + if (topIsBlackStreak || (topLeftIsDark && topRightIsDark && + isDark(image.getPixel(left - offsetX, top)) && isDark(image.getPixel(right + offsetX, top)) && + (topMidIsDark || overallBlackPixels > 9))) return GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(blackPixel, blackPixel, backgroundColor, backgroundColor)) - else if (bottomIsBlackStreak || (botLeftIsDark && botRightIsDark - && isDark(image.getPixel(left - offsetX, bot)) && isDark(image.getPixel(right + offsetX, bot)) - && (isDark(image.getPixel(midX, bot)) || overallBlackPixels > 9))) + else if (bottomIsBlackStreak || (botLeftIsDark && botRightIsDark && + isDark(image.getPixel(left - offsetX, bot)) && isDark(image.getPixel(right + offsetX, bot)) && + (isDark(image.getPixel(midX, bot)) || overallBlackPixels > 9))) return GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel)) return ColorDrawable(backgroundColor) @@ -206,7 +208,7 @@ object ImageUtil { } private fun pixelIsClose(color1: Int, color2: Int): Boolean { - return abs(Color.red(color1) - Color.red(color2)) < 30 && + return abs(Color.red(color1) - Color.red(color2)) < 30 && abs(Color.green(color1) - Color.green(color2)) < 30 && abs(Color.blue(color1) - Color.blue(color2)) < 30 } @@ -236,5 +238,4 @@ object ImageUtil { GIF("image/gif", "gif"), WEBP("image/webp", "webp") } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt index 420fb135ae..ad515f830e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt @@ -55,8 +55,8 @@ object LocaleHelper { fun getDisplayName(lang: String?, context: Context): String { return when (lang) { null -> "" - "" -> context.getString(R.string.other_source) - "all" -> context.getString(R.string.all_lang) + "" -> context.getString(R.string.other) + "all" -> context.getString(R.string.all) else -> { val locale = getLocale(lang) locale.getDisplayName(locale).capitalize() @@ -141,5 +141,4 @@ object LocaleHelper { } return newConfig } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/RxUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/RxUtil.kt index a1d583ef89..25d761d07b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/RxUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/RxUtil.kt @@ -23,4 +23,4 @@ suspend fun Single.await(subscribeOn: Scheduler? = null): T { sub.unsubscribe() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt new file mode 100644 index 0000000000..6671eb2584 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.util.system + +import androidx.appcompat.app.AppCompatDelegate + +object ThemeUtil { + fun isBlueTheme(theme: Int): Boolean { + return theme == 4 || theme == 8 || theme == 7 + } + + fun isAMOLEDTheme(theme: Int): Boolean { + return theme == 3 || theme == 6 + } + + fun nightMode(theme: Int): Int { + return when (theme) { + 1, 8 -> AppCompatDelegate.MODE_NIGHT_NO + 2, 3, 4 -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt index 7e1f840cdc..0699214c88 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt @@ -2,7 +2,11 @@ package eu.kanade.tachiyomi.util.system import android.annotation.TargetApi import android.os.Build -import android.webkit.* +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient @Suppress("OverridingDeprecatedMember") abstract class WebViewClientCompat : WebViewClient() { @@ -16,18 +20,18 @@ abstract class WebViewClientCompat : WebViewClient() { } open fun onReceivedErrorCompat( - view: WebView, - errorCode: Int, - description: String?, - failingUrl: String, - isMainFrame: Boolean) { - + view: WebView, + errorCode: Int, + description: String?, + failingUrl: String, + isMainFrame: Boolean + ) { } @TargetApi(Build.VERSION_CODES.N) final override fun shouldOverrideUrlLoading( - view: WebView, - request: WebResourceRequest + view: WebView, + request: WebResourceRequest ): Boolean { return shouldOverrideUrlCompat(view, request.url.toString()) } @@ -37,46 +41,45 @@ abstract class WebViewClientCompat : WebViewClient() { } final override fun shouldInterceptRequest( - view: WebView, - request: WebResourceRequest + view: WebView, + request: WebResourceRequest ): WebResourceResponse? { return shouldInterceptRequestCompat(view, request.url.toString()) } final override fun shouldInterceptRequest( - view: WebView, - url: String + view: WebView, + url: String ): WebResourceResponse? { return shouldInterceptRequestCompat(view, url) } @TargetApi(Build.VERSION_CODES.M) final override fun onReceivedError( - view: WebView, - request: WebResourceRequest, - error: WebResourceError + view: WebView, + request: WebResourceRequest, + error: WebResourceError ) { onReceivedErrorCompat(view, error.errorCode, error.description?.toString(), request.url.toString(), request.isForMainFrame) } final override fun onReceivedError( - view: WebView, - errorCode: Int, - description: String?, - failingUrl: String + view: WebView, + errorCode: Int, + description: String?, + failingUrl: String ) { onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url) } @TargetApi(Build.VERSION_CODES.M) final override fun onReceivedHttpError( - view: WebView, - request: WebResourceRequest, - error: WebResourceResponse + view: WebView, + request: WebResourceRequest, + error: WebResourceResponse ) { onReceivedErrorCompat(view, error.statusCode, error.reasonPhrase, request.url .toString(), request.isForMainFrame) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt new file mode 100644 index 0000000000..83e864a05a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.util.system + +import android.webkit.WebView + +private val WEBVIEW_UA_VERSION_REGEX by lazy { + Regex(""".*Chrome/(\d+)\..*""") +} + +private const val MINIMUM_WEBVIEW_VERSION = 70 + +fun WebView.isOutdated(): Boolean { + return getWebviewMajorVersion(this) < MINIMUM_WEBVIEW_VERSION +} + +// Based on https://stackoverflow.com/a/29218966 +private fun getWebviewMajorVersion(webview: WebView): Int { + val originalUA: String = webview.settings.userAgentString + + // Next call to getUserAgentString() will get us the default + webview.settings.userAgentString = null + + val uaRegexMatch = WEBVIEW_UA_VERSION_REGEX.matchEntire(webview.settings.userAgentString) + val webViewVersion: Int = if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) { + uaRegexMatch.groupValues[1].toInt() + } else { + 0 + } + + // Revert to original UA string + webview.settings.userAgentString = originalUA + + return webViewVersion +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/DeferredField.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/DeferredField.kt index 1283c6a86a..9fa71df28b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/DeferredField.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/DeferredField.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.util.view - import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt index e27f28dd5e..69335bc5b5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt @@ -1,8 +1,8 @@ package eu.kanade.tachiyomi.util.view +import android.widget.ImageView import androidx.annotation.DrawableRes import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import android.widget.ImageView /** * Set a vector on a [ImageView]. diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 98645b0281..c81a7f845e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -2,25 +2,48 @@ package eu.kanade.tachiyomi.util.view +import android.animation.ValueAnimator +import android.app.Activity import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration import android.graphics.Color import android.graphics.Point import android.graphics.Typeface +import android.os.Build import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver import android.view.WindowInsets +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.FrameLayout +import android.widget.ImageView import android.widget.TextView import androidx.annotation.Px -import androidx.annotation.RequiresApi -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.appcompat.widget.SearchView +import androidx.core.graphics.ColorUtils +import androidx.core.math.MathUtils.clamp import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.amulyakhare.textdrawable.TextDrawable -import eu.kanade.tachiyomi.R import com.amulyakhare.textdrawable.util.ColorGenerator import com.bluelinelabs.conductor.Controller +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.getResourceColor +import kotlinx.android.synthetic.main.main_activity.* +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import kotlin.math.abs import kotlin.math.min /** @@ -38,47 +61,53 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2) * @param length the duration of the snack. * @param f a function to execute in the snack, allowing for example to define a custom action. */ -fun View.snack(message: String, length: Int = Snackbar.LENGTH_SHORT, f: (Snackbar.() -> -Unit)? = null): Snackbar { +fun View.snack( + message: String, + length: Int = Snackbar.LENGTH_SHORT, + f: (Snackbar.() -> Unit)? = null +): Snackbar { val snack = Snackbar.make(this, message, length) - val textView: TextView = snack.view.findViewById(com.google.android.material.R.id.snackbar_text) - textView.setTextColor(context.getResourceColor(R.attr.snackbar_text)) - /* when { - Build.VERSION.SDK_INT >= 23 -> { - val leftM = if (this is CoordinatorLayout) 0 else rootWindowInsets.systemWindowInsetLeft - val rightM = if (this is CoordinatorLayout) 0 - else rootWindowInsets.systemWindowInsetRight - snack.config(context, rootWindowInsets - .systemWindowInsetBottom, rightM, leftM) - } - else -> snack.config(context) - }*/ - snack.config(context) + /* when { + Build.VERSION.SDK_INT >= 23 -> { + val leftM = if (this is CoordinatorLayout) 0 else rootWindowInsets.systemWindowInsetLeft + val rightM = if (this is CoordinatorLayout) 0 + else rootWindowInsets.systemWindowInsetRight + snack.config(context, rootWindowInsets + .systemWindowInsetBottom, rightM, leftM) + } + else -> snack.config(context) + }*/ if (f != null) { snack.f() } - // if (Build.VERSION.SDK_INT < 23) { - val view = if (this !is CoordinatorLayout) this else snack.view - view.doOnApplyWindowInsets { _, insets, _ -> - snack.view.updateLayoutParams { - bottomMargin = 12 + insets.systemWindowInsetBottom - } - } - /*} - else { - snack.view.doOnApplyWindowInsets { _,_,_ -> } - }*/ + val theme = Injekt.get().theme() + if (theme == 3) { + val textView: TextView = + snack.view.findViewById(com.google.android.material.R.id.snackbar_text) + val button: Button? = + snack.view.findViewById(com.google.android.material.R.id.snackbar_action) + textView.setTextColor(context.getResourceColor(R.attr.snackbar_text)) + button?.setTextColor(context.getResourceColor(R.attr.snackbar_text)) + snack.config(context) + } snack.show() return snack } -fun View.snack(resource: Int, length: Int = Snackbar.LENGTH_SHORT, f: (Snackbar.() -> -Unit)? = null): Snackbar { +fun View.snack( + resource: Int, + length: Int = Snackbar.LENGTH_SHORT, + f: (Snackbar.() -> Unit)? = null +): Snackbar { return snack(context.getString(resource), length, f) } -fun Snackbar.config(context: Context, bottomMargin: Int = 0, rightMargin: Int = 0, leftMargin: -Int = 0) { +fun Snackbar.config( + context: Context, + bottomMargin: Int = 0, + rightMargin: Int = 0, + leftMargin: Int = 0 +) { val params = this.view.layoutParams as ViewGroup.MarginLayoutParams params.setMargins(12 + leftMargin, 12, 12 + rightMargin, 12 + bottomMargin) this.view.layoutParams = params @@ -108,22 +137,29 @@ inline fun View.visibleIf(block: () -> Boolean) { visibility = if (block()) View.VISIBLE else View.GONE } +inline fun View.visibleIf(show: Boolean) { + visibility = if (show) View.VISIBLE else View.GONE +} + +inline fun View.visInvisIf(show: Boolean) { + visibility = if (show) View.VISIBLE else View.INVISIBLE +} + /** * Returns a TextDrawable determined by input * * @param text text of [TextDrawable] * @param random random color */ -fun View.getRound(text: String, random : Boolean = true): TextDrawable { +fun ImageView.roundTextIcon(text: String) { val size = min(this.width, this.height) - return TextDrawable.builder() - .beginConfig() - .width(size) - .height(size) - .textColor(Color.WHITE) - .useFont(Typeface.DEFAULT) - .endConfig() - .buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text)) + val letter = text.take(1).toUpperCase() + setImageDrawable( + TextDrawable.builder().beginConfig().width(size).height(size).textColor(Color.WHITE) + .useFont(Typeface.DEFAULT).endConfig().buildRound( + letter, ColorGenerator.MATERIAL.getColor(letter) + ) + ) } inline val View.marginTop: Int @@ -140,8 +176,32 @@ inline val View.marginLeft: Int object RecyclerWindowInsetsListener : View.OnApplyWindowInsetsListener { override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { - v.setPadding(0,0,0,insets.systemWindowInsetBottom) - //v.updatePaddingRelative(bottom = v.paddingBottom + insets.systemWindowInsetBottom) + v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) + // v.updatePaddingRelative(bottom = v.paddingBottom + insets.systemWindowInsetBottom) + return insets + } +} + +object ControllerViewWindowInsetsListener : View.OnApplyWindowInsetsListener { + override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { + v.updateLayoutParams { + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = v.context.obtainStyledAttributes(attrsArray) + topMargin = insets.systemWindowInsetTop + array.getDimensionPixelSize(0, 0) + array.recycle() + } + return insets + } +} + +object HeightTopWindowInsetsListener : View.OnApplyWindowInsetsListener { + override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { + val topInset = insets.systemWindowInsetTop + v.setPadding(0, topInset, 0, 0) + if (v.layoutParams.height != topInset) { + v.layoutParams.height = topInset + v.requestLayout() + } return insets } } @@ -156,6 +216,44 @@ fun View.doOnApplyWindowInsets(f: (View, WindowInsets, ViewPaddingState) -> Unit requestApplyInsetsWhenAttached() } +fun View.applyWindowInsetsForController() { + setOnApplyWindowInsetsListener(ControllerViewWindowInsetsListener) + requestApplyInsetsWhenAttached() +} + +fun View.checkHeightThen(f: () -> Unit) { + viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + if (height > 0) { + viewTreeObserver.removeOnGlobalLayoutListener(this) + f() + } + } + }) +} + +fun View.applyWindowInsetsForRootController(bottomNav: View) { + viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + if (bottomNav.height > 0) { + viewTreeObserver.removeOnGlobalLayoutListener(this) + setOnApplyWindowInsetsListener { view, insets -> + view.updateLayoutParams { + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = view.context.obtainStyledAttributes(attrsArray) + // topMargin = insets.systemWindowInsetTop + array + // .getDimensionPixelSize(0, 0) + bottomMargin = bottomNav.height + array.recycle() + } + insets + } + requestApplyInsetsWhenAttached() + } + } + }) +} + fun View.requestApplyInsetsWhenAttached() { if (isAttachedToWindow) { requestApplyInsets() @@ -185,8 +283,14 @@ inline fun View.updatePadding( setPadding(left, top, right, bottom) } -private fun createStateForView(view: View) = ViewPaddingState(view.paddingLeft, - view.paddingTop, view.paddingRight, view.paddingBottom, view.paddingStart, view.paddingEnd) +private fun createStateForView(view: View) = ViewPaddingState( + view.paddingLeft, + view.paddingTop, + view.paddingRight, + view.paddingBottom, + view.paddingStart, + view.paddingEnd +) data class ViewPaddingState( val left: Int, @@ -197,23 +301,140 @@ data class ViewPaddingState( val end: Int ) - -fun Controller.setOnQueryTextChangeListener(searchView: SearchView, f: (text: String?) -> Boolean) { +fun Controller.setOnQueryTextChangeListener( + searchView: SearchView, + onlyOnSubmit: Boolean = false, + f: (text: String?) -> Boolean +) { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String?): Boolean { - if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) { + if (!onlyOnSubmit && router.backstack.lastOrNull() + ?.controller() == this@setOnQueryTextChangeListener + ) { return f(newText) } - return true + return false } override fun onQueryTextSubmit(query: String?): Boolean { + if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) { + val imm = + activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + ?: return f(query) + imm.hideSoftInputFromWindow(searchView.windowToken, 0) + return f(query) + } return true } }) } -@RequiresApi(17) +fun Controller.scrollViewWith( + recycler: RecyclerView, + padBottom: Boolean = false, + customPadding: Boolean = false, + skipFirstSnap: Boolean = false, + swipeRefreshLayout: SwipeRefreshLayout? = null, + afterInsets: ((WindowInsets) -> Unit)? = null +) { + var statusBarHeight = -1 + activity?.appbar?.y = 0f + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = recycler.context.obtainStyledAttributes(attrsArray) + val appBarHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + swipeRefreshLayout?.setDistanceToTriggerSync(150.dpToPx) + recycler.doOnApplyWindowInsets { view, insets, _ -> + val headerHeight = insets.systemWindowInsetTop + appBarHeight + if (!customPadding) view.updatePaddingRelative( + top = headerHeight, + bottom = if (padBottom) insets.systemWindowInsetBottom else view.paddingBottom + ) + swipeRefreshLayout?.setProgressViewOffset( + true, headerHeight + (-60).dpToPx, headerHeight + 10.dpToPx + ) + statusBarHeight = insets.systemWindowInsetTop + afterInsets?.invoke(insets) + } + var elevationAnim: ValueAnimator? = null + var elevate = false + val elevateFunc: (Boolean) -> Unit = { el -> + elevate = el + elevationAnim?.cancel() + elevationAnim = ValueAnimator.ofFloat( + activity!!.appbar.elevation, if (el) 15f else 0f + ) + elevationAnim?.addUpdateListener { valueAnimator -> + activity!!.appbar.elevation = valueAnimator.animatedValue as Float + } + elevationAnim?.start() + } + addLifecycleListener(object : Controller.LifecycleListener() { + override fun onChangeStart( + controller: Controller, + changeHandler: ControllerChangeHandler, + changeType: ControllerChangeType + ) { + super.onChangeStart(controller, changeHandler, changeType) + if (changeType.isEnter) + elevateFunc(elevate) + } + }) + elevateFunc(recycler.canScrollVertically(-1)) + recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (router?.backstack?.lastOrNull() + ?.controller() == this@scrollViewWith && statusBarHeight > -1 && activity != null && activity!!.appbar.height > 0 + ) { + if (!recycler.canScrollVertically(-1)) { + val shortAnimationDuration = resources?.getInteger( + android.R.integer.config_shortAnimTime + ) ?: 0 + activity!!.appbar.animate().y(0f).setDuration(shortAnimationDuration.toLong()) + .start() + if (elevate) elevateFunc(false) + } else { + activity!!.appbar.y -= dy + activity!!.appbar.y = clamp( + activity!!.appbar.y, -activity!!.appbar.height.toFloat(), 0f + ) + if ((activity!!.appbar.y <= -activity!!.appbar.height.toFloat() || + dy == 0 && activity!!.appbar.y == 0f) && !elevate) + elevateFunc(true) + } + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + if (router?.backstack?.lastOrNull() + ?.controller() == this@scrollViewWith && statusBarHeight > -1 && activity != null && activity!!.appbar.height > 0 + ) { + val halfWay = abs((-activity!!.appbar.height.toFloat()) / 2) + val shortAnimationDuration = resources?.getInteger( + android.R.integer.config_shortAnimTime + ) ?: 0 + val closerToTop = abs(activity!!.appbar.y) - halfWay > 0 + val atTop = (!customPadding && + (recycler.layoutManager as LinearLayoutManager) + .findFirstVisibleItemPosition() < 2 && !skipFirstSnap) || + !recycler.canScrollVertically(-1) + activity!!.appbar.animate().y( + if (closerToTop && !atTop) (-activity!!.appbar.height.toFloat()) + else 0f + ).setDuration(shortAnimationDuration.toLong()).start() + if (recycler.canScrollVertically(-1) && !elevate) + elevateFunc(true) + else if (!recycler.canScrollVertically(-1) && elevate) + elevateFunc(false) + } + } + } + }) +} + inline fun View.updatePaddingRelative( @Px start: Int = paddingStart, @Px top: Int = paddingTop, @@ -221,4 +442,64 @@ inline fun View.updatePaddingRelative( @Px bottom: Int = paddingBottom ) { setPaddingRelative(start, top, end, bottom) -} \ No newline at end of file +} + +fun BottomSheetDialog.setEdgeToEdge( + activity: Activity, + contentView: View, + setTopMargin: Int = -1 +) { + window?.setBackgroundDrawable(null) + val currentNightMode = + activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + window?.navigationBarColor = activity.window.navigationBarColor + val isLight = (activity.window?.decorView?.systemUiVisibility ?: 0) and View + .SYSTEM_UI_FLAG_LIGHT_STATUS_BAR == View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLight) + window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + window?.findViewById(com.google.android.material.R.id.container)?.fitsSystemWindows = + false + contentView.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // + // or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + + if (activity.window.decorView.rootWindowInsets.systemWindowInsetLeft + + activity.window.decorView.rootWindowInsets.systemWindowInsetRight == 0) + contentView.systemUiVisibility = contentView.systemUiVisibility + .or(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) + /*contentView.updateLayoutParams { + leftMargin = activity.window.decorView.rootWindowInsets.systemWindowInsetLeft + rightMargin = activity.window.decorView.rootWindowInsets.systemWindowInsetRight + }*/ + if (setTopMargin > 0) (contentView.parent as View).updateLayoutParams { + height = + activity.window.decorView.height - activity.window.decorView.rootWindowInsets.systemWindowInsetTop - setTopMargin + // activity.window.decorView.rootWindowInsets.systemWindowInsetTop // + setTopMargin + } + else if (setTopMargin == 0) contentView.updateLayoutParams { + topMargin = activity.window.decorView.rootWindowInsets.systemWindowInsetTop + } + contentView.requestLayout() +} + +fun setBottomEdge(view: View, activity: Activity) { + val marginB = view.marginBottom + view.updateLayoutParams { + bottomMargin = marginB + activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + } +} + +fun SwipeRefreshLayout.setStyle() { + setColorSchemeColors(context.getResourceColor(R.attr.actionBarTintColor)) + setProgressBackgroundColorSchemeColor(context.getResourceColor(R.attr.colorPrimaryVariant)) +} + +fun MaterialButton.resetStrokeColor() { + strokeColor = ColorStateList.valueOf( + ColorUtils.setAlphaComponent( + context.getResourceColor( + R.attr.colorOnSurface + ), 31 + ) + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt index 1da0b4d986..64c71200a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.util.view -import androidx.annotation.LayoutRes import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.LayoutRes /** * Extension method to inflate a view directly from its parent. diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt index 738e2e70f9..dd3ebcd522 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt @@ -3,15 +3,21 @@ package eu.kanade.tachiyomi.widget import android.content.Context import android.util.AttributeSet import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.util.system.pxToDp import kotlin.math.max +import kotlin.math.roundToInt class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : androidx.recyclerview.widget.RecyclerView(context, attrs) { - private val manager = androidx.recyclerview.widget.GridLayoutManager(context, 1) + val manager = GridLayoutManager(context, 1) - private var columnWidth = -1 + var columnWidth = -1f + set(value) { + field = value + if (measuredWidth > 0) + setSpan(true) + } var spanCount = 0 set(value) { @@ -22,25 +28,34 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att } val itemWidth: Int - get() = measuredWidth / manager.spanCount - - init { - if (attrs != null) { - val attrsArray = intArrayOf(android.R.attr.columnWidth) - val array = context.obtainStyledAttributes(attrs, attrsArray) - columnWidth = array.getDimensionPixelSize(0, -1) - array.recycle() + get() { + return if (spanCount == 0) measuredWidth / getTempSpan() + else measuredWidth / manager.spanCount } + init { layoutManager = manager } + private fun getTempSpan(): Int { + if (spanCount == 0 && columnWidth > 0) { + val dpWidth = (measuredWidth.pxToDp / 100f).roundToInt() + return max(1, (dpWidth / columnWidth).roundToInt()) + } + return 3 + } + override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) - if (spanCount == 0 && columnWidth > 0) { - val count = max(1, measuredWidth / columnWidth) + setSpan() + } + + private fun setSpan(force: Boolean = false) { + if ((spanCount == 0 || force) && columnWidth > 0) { + val dpWidth = (measuredWidth.pxToDp / 100f).roundToInt() + val count = max(1, (dpWidth / columnWidth).roundToInt()) spanCount = count +// Timber.d("Dp width: $dpWidth - RSpan: $spanCount") } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt index ccefd385c4..2f71a21077 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.widget -import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import com.nononsenseapps.filepicker.AbstractFilePickerFragment import com.nononsenseapps.filepicker.FilePickerActivity import com.nononsenseapps.filepicker.FilePickerFragment diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt index b6195a7d9a..adbe88ac75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt @@ -1,13 +1,12 @@ package eu.kanade.tachiyomi.widget import android.content.Context -import androidx.annotation.StringRes import android.util.AttributeSet import android.widget.LinearLayout +import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.view.inflate -import kotlinx.android.synthetic.main.common_dialog_with_checkbox.view.checkbox_option -import kotlinx.android.synthetic.main.common_dialog_with_checkbox.view.description +import kotlinx.android.synthetic.main.common_dialog_with_checkbox.view.* class DialogCheckboxView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { @@ -16,11 +15,11 @@ class DialogCheckboxView @JvmOverloads constructor(context: Context, attrs: Attr addView(inflate(R.layout.common_dialog_with_checkbox)) } - fun setDescription(@StringRes id: Int){ + fun setDescription(@StringRes id: Int) { description.text = context.getString(id) } - fun setOptionDescription(@StringRes id: Int){ + fun setOptionDescription(@StringRes id: Int) { checkbox_option.text = context.getString(id) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt index 883755cff6..5e05cdb5c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt @@ -37,7 +37,6 @@ class DialogCustomDownloadView @JvmOverloads constructor(context: Context, attrs addView(inflate(R.layout.download_custom_amount)) } - /** * Called when view is added * diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt index e9a6696a5f..dad74a771a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt @@ -1,12 +1,12 @@ package eu.kanade.tachiyomi.widget -import androidx.drawerlayout.widget.DrawerLayout import android.view.View import android.view.ViewGroup +import androidx.drawerlayout.widget.DrawerLayout class DrawerSwipeCloseListener( - private val drawer: androidx.drawerlayout.widget.DrawerLayout, - private val navigationView: ViewGroup + private val drawer: androidx.drawerlayout.widget.DrawerLayout, + private val navigationView: ViewGroup ) : androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener() { override fun onDrawerOpened(drawerView: View) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt deleted file mode 100644 index 8ae15900f2..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.animation.ObjectAnimator -import android.animation.StateListAnimator -import android.content.Context -import android.os.Build -import com.google.android.material.R -import com.google.android.material.appbar.AppBarLayout -import android.util.AttributeSet - -class ElevationAppBarLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null -) : AppBarLayout(context, attrs) { - - private var origStateAnimator: StateListAnimator? = null - - init { - origStateAnimator = stateListAnimator - } - - fun enableElevation() { - stateListAnimator = origStateAnimator - } - - fun disableElevation() { - stateListAnimator = StateListAnimator().apply { - val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f) - - // Enabled and collapsible, but not collapsed means not elevated - addState(intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed), - objAnimator) - - // Default enabled state - addState(intArrayOf(android.R.attr.enabled), objAnimator) - - // Disabled state - addState(IntArray(0), objAnimator) - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt index 95d5c1f273..23e75cae9f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt @@ -7,11 +7,10 @@ import android.widget.RelativeLayout import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setVectorCompat -import kotlinx.android.synthetic.main.common_view_empty.view.image_view -import kotlinx.android.synthetic.main.common_view_empty.view.text_label +import kotlinx.android.synthetic.main.common_view_empty.view.* class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - RelativeLayout (context, attrs) { + RelativeLayout(context, attrs) { init { inflate(context, R.layout.common_view_empty, this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt index af5da9612c..e5e5bb9ff2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt @@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.widget import android.content.Context import android.graphics.drawable.Drawable -import androidx.annotation.CallSuper -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView import android.util.AttributeSet import android.view.View import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.getResourceColor @@ -17,10 +17,11 @@ import eu.kanade.tachiyomi.util.system.getResourceColor * inflation and allowing customizable items (multiple selections, custom views, etc). */ open class ExtendedNavigationView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : SimpleNavigationView(context, attrs, defStyleAttr) { + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + SimpleNavigationView(context, attrs, defStyleAttr) { /** * Every item of the nav view. Generic items must belong to this list, custom items could be @@ -46,15 +47,15 @@ open class ExtendedNavigationView @JvmOverloads constructor( /** * A checkbox belonging to a group. The group must handle selections and restrictions. */ - class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false) - : Checkbox(resTitle, checked), GroupedItem + class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false) : + Checkbox(resTitle, checked), GroupedItem /** * A radio belonging to a group (a sole radio makes no sense). The group must handle * selections and restrictions. */ - class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false) - : Item(), GroupedItem + class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false) : + Item(), GroupedItem /** * An item with which needs more than two states (selected/deselected). @@ -95,8 +96,8 @@ open class ExtendedNavigationView @JvmOverloads constructor( * An item with which needs more than two states (selected/deselected) belonging to a group. * The group must handle selections and restrictions. */ - abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0) - : MultiState(resTitle, state), GroupedItem + abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0) : + MultiState(resTitle, state), GroupedItem /** * A multistate item for sorting lists (unselected, ascending, descending). @@ -117,7 +118,6 @@ open class ExtendedNavigationView @JvmOverloads constructor( else -> null } } - } class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) { @@ -126,10 +126,11 @@ open class ExtendedNavigationView @JvmOverloads constructor( const val STATE_IGNORE = 0 const val STATE_INCLUDE = 1 const val STATE_EXCLUDE = 2 + const val STATE_REALLY_EXCLUDE = 3 } override fun getStateDrawable(context: Context): Drawable? { - return when(state) { + return when (state) { STATE_INCLUDE -> tintVector(context, R.drawable.ic_check_box_24dp) STATE_EXCLUDE -> tintVector(context, R.drawable.ic_check_box_x_24dp, android.R.attr.textColorSecondary) @@ -184,7 +185,6 @@ open class ExtendedNavigationView @JvmOverloads constructor( * selections of its items. */ fun onItemClicked(item: Item) - } /** @@ -263,7 +263,5 @@ open class ExtendedNavigationView @JvmOverloads constructor( } abstract fun onItemClicked(item: Item) - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/FABMoveBehaviour.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/FABMoveBehaviour.kt index 6cee334986..0beb02e4e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/FABMoveBehaviour.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/FABMoveBehaviour.kt @@ -38,4 +38,4 @@ class FABMoveBehaviour(context: Context, attrs: AttributeSet) : } return minOffset } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/IgnoreFirstSpinnerListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/IgnoreFirstSpinnerListener.kt index eab856562a..107e6fa558 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/IgnoreFirstSpinnerListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/IgnoreFirstSpinnerListener.kt @@ -4,7 +4,7 @@ import android.view.View import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener -class IgnoreFirstSpinnerListener(private val block: (Int) -> Unit): OnItemSelectedListener { +class IgnoreFirstSpinnerListener(private val block: (Int) -> Unit) : OnItemSelectedListener { private var firstEvent = true @@ -17,6 +17,5 @@ class IgnoreFirstSpinnerListener(private val block: (Int) -> Unit): OnItemSelect } override fun onNothingSelected(parent: AdapterView<*>?) { - } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/MinMaxNumberPicker.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/MinMaxNumberPicker.kt index 07adb8d0c5..bdc7d79d14 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/MinMaxNumberPicker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/MinMaxNumberPicker.kt @@ -20,4 +20,3 @@ class MinMaxNumberPicker @JvmOverloads constructor(context: Context, attrs: Attr } } } - diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt index 30e0eac7c5..9ab0cc9c11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/NegativeSeekBar.kt @@ -7,7 +7,6 @@ import android.widget.SeekBar import eu.kanade.tachiyomi.R import kotlin.math.abs - class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : SeekBar(context, attrs) { @@ -66,5 +65,4 @@ class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: Attribu super.onRestoreInstanceState(state) super.setProgress(origProgress) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt index 79b2057778..b7ab3a82fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt @@ -12,30 +12,30 @@ import androidx.annotation.Dimension * A class that draws the outlines of a text when given a stroke color and stroke width. */ class OutlineSpan( - @ColorInt private val strokeColor: Int, - @Dimension private val strokeWidth: Float + @ColorInt private val strokeColor: Int, + @Dimension private val strokeWidth: Float ) : ReplacementSpan() { override fun getSize( - paint: Paint, - text: CharSequence, - start: Int, - end: Int, - fm: Paint.FontMetricsInt? + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? ): Int { return paint.measureText(text.toString().substring(start until end)).toInt() } override fun draw( - canvas: Canvas, - text: CharSequence, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint ) { val originTextColor = paint.color @@ -53,5 +53,4 @@ class OutlineSpan( canvas.drawText(text, start, end, x, y.toFloat(), paint) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/PTSansTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/PTSansTextView.kt index 8bbf4ad243..be672fde1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/PTSansTextView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/PTSansTextView.kt @@ -4,13 +4,12 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Typeface import android.util.AttributeSet -import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView import eu.kanade.tachiyomi.R -import java.util.* - +import java.util.HashMap class PTSansTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - TextView(context, attrs) { + AppCompatTextView(context, attrs) { companion object { const val PTSANS_NARROW = 0 @@ -43,5 +42,4 @@ class PTSansTextView @JvmOverloads constructor(context: Context, attrs: Attribut super.onDraw(canvas) super.onDraw(canvas) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/PreCachingLayoutManager.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/PreCachingLayoutManager.kt index 9469c5e8cd..1a0b1bfb1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/PreCachingLayoutManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/PreCachingLayoutManager.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.widget import android.content.Context -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView class PreCachingLayoutManager(context: Context) : androidx.recyclerview.widget.LinearLayoutManager(context) { @@ -22,5 +20,4 @@ class PreCachingLayoutManager(context: Context) : androidx.recyclerview.widget.L } return DEFAULT_EXTRA_LAYOUT_SPACE } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt index f644e3fdd5..2e1d51c2f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.widget import android.view.View import android.view.ViewGroup import com.nightlynexus.viewstatepageradapter.ViewStatePagerAdapter -import java.util.* +import java.util.Stack abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() { @@ -31,6 +31,4 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() { recycleView(view, position) if (recycle) pool.push(view) } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt index a78a678034..9a79b0c68c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt @@ -64,5 +64,4 @@ class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: Att anim.start() return true } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleAnimationListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleAnimationListener.kt index 12a2b4ef22..853e8ba0bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleAnimationListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleAnimationListener.kt @@ -8,4 +8,4 @@ open class SimpleAnimationListener : Animation.AnimationListener { override fun onAnimationEnd(animation: Animation) {} override fun onAnimationStart(animation: Animation) {} -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt index 49e396efd6..91c2d487f1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt @@ -23,10 +23,11 @@ import eu.kanade.tachiyomi.R as TR @Suppress("LeakingThis") @SuppressLint("PrivateResource", "RestrictedApi") open class SimpleNavigationView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : ScrimInsetsFrameLayout(context, attrs, defStyleAttr) { + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + ScrimInsetsFrameLayout(context, attrs, defStyleAttr) { /** * Max width of the navigation view. @@ -87,19 +88,18 @@ open class SimpleNavigationView @JvmOverloads constructor( /** * Separator view holder. */ - class SeparatorHolder(parent: ViewGroup) - : Holder(parent.inflate(R.layout.design_navigation_item_separator)) + class SeparatorHolder(parent: ViewGroup) : + Holder(parent.inflate(R.layout.design_navigation_item_separator)) /** * Header view holder. */ - class HeaderHolder(parent: ViewGroup) - : Holder(parent.inflate(TR.layout.navigation_view_group)){ + class HeaderHolder(parent: ViewGroup) : + Holder(parent.inflate(TR.layout.navigation_view_group)) { val title: TextView = itemView.findViewById(TR.id.title) } - /** * Clickable view holder. */ @@ -112,8 +112,8 @@ open class SimpleNavigationView @JvmOverloads constructor( /** * Radio view holder. */ - class RadioHolder(parent: ViewGroup, listener: View.OnClickListener?) - : ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) { + class RadioHolder(parent: ViewGroup, listener: View.OnClickListener?) : + ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) { val radio: RadioButton = itemView.findViewById(TR.id.nav_view_item) } @@ -121,8 +121,8 @@ open class SimpleNavigationView @JvmOverloads constructor( /** * Checkbox view holder. */ - class CheckboxHolder(parent: ViewGroup, listener: View.OnClickListener?) - : ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) { + class CheckboxHolder(parent: ViewGroup, listener: View.OnClickListener?) : + ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) { val check: CheckBox = itemView.findViewById(TR.id.nav_view_item) } @@ -130,21 +130,21 @@ open class SimpleNavigationView @JvmOverloads constructor( /** * Multi state view holder. */ - class MultiStateHolder(parent: ViewGroup, listener: View.OnClickListener?) - : ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) { + class MultiStateHolder(parent: ViewGroup, listener: View.OnClickListener?) : + ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) { val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item) } - class SpinnerHolder(parent: ViewGroup, listener: OnClickListener? = null) - : ClickableHolder(parent.inflate(TR.layout.navigation_view_spinner), listener) { + class SpinnerHolder(parent: ViewGroup, listener: OnClickListener? = null) : + ClickableHolder(parent.inflate(TR.layout.navigation_view_spinner), listener) { val text: TextView = itemView.findViewById(TR.id.nav_view_item_text) val spinner: Spinner = itemView.findViewById(TR.id.nav_view_item) } - class EditTextHolder(parent: ViewGroup) - : Holder(parent.inflate(TR.layout.navigation_view_text)) { + class EditTextHolder(parent: ViewGroup) : + Holder(parent.inflate(TR.layout.navigation_view_text)) { val wrapper: TextInputLayout = itemView.findViewById(TR.id.nav_view_item_wrapper) val edit: EditText = itemView.findViewById(TR.id.nav_view_item) @@ -159,5 +159,4 @@ open class SimpleNavigationView @JvmOverloads constructor( const val VIEW_TYPE_TEXT = 105 const val VIEW_TYPE_LIST = 106 } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt index 77f815bd34..12e903bfb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleSeekBarListener.kt @@ -10,4 +10,4 @@ open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener { override fun onStopTrackingTouch(seekBar: SeekBar) { } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt index 5768afe90c..0613e330b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt @@ -1,10 +1,10 @@ package eu.kanade.tachiyomi.widget import android.graphics.drawable.Drawable -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import android.view.View import android.widget.ImageView import android.widget.ImageView.ScaleType +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.bumptech.glide.request.target.ImageViewTarget import com.bumptech.glide.request.transition.Transition import eu.kanade.tachiyomi.R @@ -21,10 +21,12 @@ import eu.kanade.tachiyomi.util.view.visible * @param errorDrawableRes the error drawable resource to show. * @param errorScaleType the scale type for the error drawable, [ScaleType.CENTER] by default. */ -class StateImageViewTarget(view: ImageView, - val progress: View? = null, - val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp, - val errorScaleType: ScaleType = ScaleType.CENTER) : +class StateImageViewTarget( + view: ImageView, + val progress: View? = null, + val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp, + val errorScaleType: ScaleType = ScaleType.CENTER +) : ImageViewTarget(view) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt index 6b101e17ee..d2c2a634e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.widget -import androidx.viewpager.widget.PagerAdapter import android.view.View import android.view.ViewGroup @@ -30,5 +29,4 @@ abstract class ViewPagerAdapter : androidx.viewpager.widget.PagerAdapter() { interface PositionableView { val item: Any } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListMatPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListMatPreference.kt index b423304bb8..0a92734dae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListMatPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListMatPreference.kt @@ -3,36 +3,35 @@ package eu.kanade.tachiyomi.widget.preference import android.app.Activity import android.content.Context import android.util.AttributeSet -import androidx.preference.Preference import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.callbacks.onDismiss import com.afollestad.materialdialogs.list.listItemsSingleChoice -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.setting.defaultValue -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -class IntListMatPreference @JvmOverloads constructor(activity: Activity?, context: Context, +class IntListMatPreference @JvmOverloads constructor( + activity: Activity?, + context: Context, attrs: AttributeSet? = - null) : + null +) : MatPreference(activity, context, attrs) { - var entryValues:List = emptyList() - var entryRange:IntRange + var entryValues: List = emptyList() + var entryRange: IntRange get() = 0..0 set(value) { entryValues = value.toList() } - var entriesRes:Array + var entriesRes: Array get() = emptyArray() set(value) { entries = value.map { context.getString(it) } } - private var defValue:Int = 0 - var entries:List = emptyList() + private var defValue: Int = 0 + var entries: List = emptyList() override fun onSetInitialValue(defaultValue: Any?) { super.onSetInitialValue(defaultValue) defValue = defaultValue as? Int ?: defValue } override fun getSummary(): CharSequence { + if (customSummary != null) return customSummary!! + if (key == null) return super.getSummary() val index = entryValues.indexOf(prefs.getInt(key, defValue).getOrDefault()) return if (entries.isEmpty() || index == -1) "" else entries[index] @@ -46,11 +45,12 @@ AttributeSet? = initialSelection = default) { _, pos, _ -> val value = entryValues[pos] - prefs.getInt(key, defValue).set(value) + if (key != null) + prefs.getInt(key, defValue).set(value) callChangeListener(value) this@IntListMatPreference.summary = this@IntListMatPreference.summary dismiss() } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListPreference.kt index 3e3000a003..37873398f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/IntListPreference.kt @@ -1,8 +1,8 @@ package eu.kanade.tachiyomi.widget.preference import android.content.Context -import androidx.preference.ListPreference import android.util.AttributeSet +import androidx.preference.ListPreference class IntListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ListMatPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ListMatPreference.kt index 8fad529a36..352484eada 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ListMatPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ListMatPreference.kt @@ -3,37 +3,35 @@ package eu.kanade.tachiyomi.widget.preference import android.annotation.SuppressLint import android.app.Activity import android.content.Context -import android.content.SharedPreferences import android.util.AttributeSet import androidx.preference.Preference -import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.setting.defaultValue -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -open class ListMatPreference @JvmOverloads constructor(activity: Activity?, context: Context, +open class ListMatPreference @JvmOverloads constructor( + activity: Activity?, + context: Context, attrs: AttributeSet? = - null) : + null +) : MatPreference(activity, context, attrs) { - var sharedPref:String? = null - var otherPref:Preference? = null - var entryValues:List = emptyList() - var entriesRes:Array + var sharedPref: String? = null + var otherPref: Preference? = null + var entryValues: List = emptyList() + var entriesRes: Array get() = emptyArray() set(value) { entries = value.map { context.getString(it) } } - protected var defValue:String = "" - var entries:List = emptyList() + protected var defValue: String = "" + var entries: List = emptyList() override fun onSetInitialValue(defaultValue: Any?) { super.onSetInitialValue(defaultValue) defValue = defaultValue as? String ?: defValue } override fun getSummary(): CharSequence { + if (customSummary != null) return customSummary!! val index = entryValues.indexOf(prefs.getStringPref(key, defValue).getOrDefault()) return if (entries.isEmpty() || index == -1) "" else entries[index] @@ -50,8 +48,7 @@ open class ListMatPreference @JvmOverloads constructor(activity: Activity?, cont val default = entryValues.indexOf(if (sharedPref != null) { val settings = context.getSharedPreferences(sharedPref, Context.MODE_PRIVATE) settings.getString(key, "") - } - else prefs.getStringPref(key, defValue).getOrDefault()) + } else prefs.getStringPref(key, defValue).getOrDefault()) listItemsSingleChoice(items = entries, waitForPositiveButton = false, initialSelection = default) { _, pos, _ -> @@ -68,8 +65,7 @@ open class ListMatPreference @JvmOverloads constructor(activity: Activity?, cont else otherPref?.summary = otherPref?.summary?.toString()?.replace(oldDef, entries[pos] ) ?: entries[pos] - } - else { + } else { prefs.getStringPref(key, defValue).set(value) this@ListMatPreference.summary = this@ListMatPreference.summary callChangeListener(value) @@ -77,4 +73,4 @@ open class ListMatPreference @JvmOverloads constructor(activity: Activity?, cont dismiss() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt index 62e1765a6a..0f5974ee1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt @@ -2,22 +2,21 @@ package eu.kanade.tachiyomi.widget.preference import android.content.Context import android.graphics.Color -import androidx.preference.CheckBoxPreference -import androidx.preference.PreferenceViewHolder import android.util.AttributeSet import android.view.View +import androidx.preference.CheckBoxPreference +import androidx.preference.PreferenceViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setVectorCompat -import kotlinx.android.synthetic.main.pref_item_source.view.login -import kotlinx.android.synthetic.main.pref_item_source.view.login_frame +import kotlinx.android.synthetic.main.pref_item_source.view.* class LoginCheckBoxPreference @JvmOverloads constructor( - context: Context, - val source: HttpSource, - attrs: AttributeSet? = null + context: Context, + val source: HttpSource, + attrs: AttributeSet? = null ) : CheckBoxPreference(context, attrs) { init { @@ -51,8 +50,7 @@ class LoginCheckBoxPreference @JvmOverloads constructor( } // Make method public - override public fun notifyChanged() { + public override fun notifyChanged() { super.notifyChanged() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt index 24b14eae0b..06b591673c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -12,18 +12,29 @@ import com.dd.processbutton.iml.ActionProcessButton import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.widget.SimpleTextWatcher import kotlinx.android.synthetic.main.pref_account_login.view.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import rx.Subscription import uy.kohesive.injekt.injectLazy -abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController(bundle) { +abstract class LoginDialogPreference( + private val usernameLabel: String? = null, + bundle: Bundle? = null +) : + DialogController(bundle) { var v: View? = null private set val preferences: PreferencesHelper by injectLazy() + val scope = CoroutineScope(Job() + Dispatchers.Main) + var requestSubscription: Subscription? = null open var canLogout = false @@ -32,9 +43,6 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController( val dialog = MaterialDialog(activity!!).apply { customView(R.layout.pref_account_login, scrollable = false) positiveButton(android.R.string.cancel) - if (canLogout) { - negativeButton(R.string.logout) { logout() } - } } onViewCreated(dialog.view) @@ -42,7 +50,7 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController( return dialog } - open fun logout() { } + open fun logout() {} fun onViewCreated(view: View) { v = view.apply { @@ -53,11 +61,20 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController( password.transformationMethod = PasswordTransformationMethod() } + if (!usernameLabel.isNullOrEmpty()) { + username_label.text = usernameLabel + } + login.setMode(ActionProcessButton.Mode.ENDLESS) login.setOnClickListener { checkLogin() } setCredentialsOnView(this) + if (canLogout && !username.text.isNullOrEmpty()) { + logout.visible() + logout.setOnClickListener { logout() } + } + show_password.isEnabled = password.text.isNullOrEmpty() password.addTextChangedListener(object : SimpleTextWatcher() { @@ -68,7 +85,6 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController( } }) } - } override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { @@ -79,11 +95,11 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController( } open fun onDialogClosed() { + scope.cancel() requestSubscription?.unsubscribe() } protected abstract fun checkLogin() protected abstract fun setCredentialsOnView(view: View) - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt index a778196d61..fc175c84fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.widget.preference import android.content.Context +import android.util.AttributeSet import androidx.preference.Preference import androidx.preference.PreferenceViewHolder -import android.util.AttributeSet import eu.kanade.tachiyomi.R import kotlinx.android.synthetic.main.pref_widget_imageview.view.* @@ -23,8 +23,7 @@ class LoginPreference @JvmOverloads constructor(context: Context, attrs: Attribu R.drawable.ic_done_green_24dp) } - override public fun notifyChanged() { + public override fun notifyChanged() { super.notifyChanged() } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt index 491c895165..e503ee6b6d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt @@ -6,20 +6,22 @@ import android.util.AttributeSet import androidx.preference.Preference import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onDismiss -import com.afollestad.materialdialogs.list.listItemsSingleChoice import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -open class MatPreference @JvmOverloads constructor(val activity: Activity?, context: Context, +open class MatPreference @JvmOverloads constructor( + val activity: Activity?, + context: Context, attrs: AttributeSet? = - null) : + null +) : Preference(context, attrs) { protected val prefs: PreferencesHelper = Injekt.get() private var isShowing = false + var customSummary: String? = null override fun onClick() { if (!isShowing) @@ -29,6 +31,10 @@ open class MatPreference @JvmOverloads constructor(val activity: Activity?, cont isShowing = true } + override fun getSummary(): CharSequence { + return customSummary ?: super.getSummary() + } + open fun dialog(): MaterialDialog { return MaterialDialog(activity ?: context).apply { if (title != null) @@ -36,4 +42,4 @@ open class MatPreference @JvmOverloads constructor(val activity: Activity?, cont negativeButton(android.R.string.cancel) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MultiListMatPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MultiListMatPreference.kt index 14ea22b901..1d35f5bf0b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MultiListMatPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MultiListMatPreference.kt @@ -11,19 +11,27 @@ import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.uncheckItem import eu.kanade.tachiyomi.data.preference.getOrDefault -class MultiListMatPreference @JvmOverloads constructor(activity: Activity?, context: Context, +class MultiListMatPreference @JvmOverloads constructor( + activity: Activity?, + context: Context, attrs: AttributeSet? = - null) : + null +) : ListMatPreference(activity, context, attrs) { - var allSelectionRes:Int? = null - var customSummaryRes:Int + var allSelectionRes: Int? = null + var customSummaryRes: Int get() = 0 set(value) { customSummary = context.getString(value) } - var customSummary:String? = null override fun getSummary(): CharSequence { - return customSummary ?: super.getSummary() + if (customSummary != null) return customSummary!! + return prefs.getStringSet(key, emptySet()).getOrDefault().mapNotNull { + if (entryValues.indexOf(it) == -1) null + else entryValues.indexOf(it) + if (allSelectionRes != null) 1 else 0 + }.toIntArray().joinToString(",") { + entries[it] + } } @SuppressLint("CheckResult") @@ -58,4 +66,4 @@ class MultiListMatPreference @JvmOverloads constructor(activity: Activity?, cont } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt index 0aa7c3828a..15ef30b8b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt @@ -7,23 +7,20 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.pref_account_login.view.dialog_title -import kotlinx.android.synthetic.main.pref_account_login.view.login -import kotlinx.android.synthetic.main.pref_account_login.view.password -import kotlinx.android.synthetic.main.pref_account_login.view.username +import kotlinx.android.synthetic.main.pref_account_login.view.* import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) { +class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle = bundle) { private val source = Injekt.get().get(args.getLong("key")) as LoginSource constructor(source: Source) : this(Bundle().apply { putLong("key", source.id) }) override fun setCredentialsOnView(view: View) = with(view) { - dialog_title.text = context.getString(R.string.login_title, source.toString()) + dialog_title.text = context.getString(R.string.log_in_to_, source.toString()) username.setText(preferences.sourceUsername(source)) password.setText(preferences.sourcePassword(source)) } @@ -47,7 +44,7 @@ class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) password.text.toString()) dialog?.dismiss() - context.toast(R.string.login_success) + context.toast(R.string.successfully_logged_in) } else { preferences.setSourceCredentials(source, "", "") login.progress = -1 @@ -68,5 +65,4 @@ class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) interface Listener { fun loginDialogClosed(source: LoginSource) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt index 24f9a68218..a4ed0194a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt @@ -4,21 +4,22 @@ import android.annotation.TargetApi import android.content.Context import android.content.res.TypedArray import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH -import androidx.preference.PreferenceCategory -import androidx.preference.PreferenceViewHolder -import androidx.appcompat.widget.SwitchCompat import android.util.AttributeSet import android.view.View import android.widget.Checkable import android.widget.CompoundButton import android.widget.TextView +import androidx.appcompat.widget.SwitchCompat +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.getResourceColor class SwitchPreferenceCategory @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null) -: PreferenceCategory( + context: Context, + attrs: AttributeSet? = null +) : +PreferenceCategory( context, attrs, R.attr.switchPreferenceCompatStyle), @@ -121,5 +122,4 @@ CompoundButton.OnCheckedChangeListener { else defaultValue as Boolean) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt index baf6806eaf..8555d1f4eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt @@ -6,31 +6,30 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.pref_account_login.view.dialog_title -import kotlinx.android.synthetic.main.pref_account_login.view.login -import kotlinx.android.synthetic.main.pref_account_login.view.password -import kotlinx.android.synthetic.main.pref_account_login.view.username -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.android.synthetic.main.pref_account_login.view.* +import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) { +class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) : + LoginDialogPreference(usernameLabel, bundle) { private val service = Injekt.get().getService(args.getInt("key"))!! override var canLogout = true - constructor(service: TrackService) : this(Bundle().apply { putInt("key", service.id) }) + constructor(service: TrackService) : this(service, null) + + constructor(service: TrackService, usernameLabel: String?) : + this(usernameLabel, Bundle().apply { putInt("key", service.id) }) override fun setCredentialsOnView(view: View) = with(view) { - dialog_title.text = context.getString(R.string.login_title, service.name) + dialog_title.text = context.getString(R.string.log_in_to_, service.name) username.setText(service.getUsername()) password.setText(service.getPassword()) } override fun checkLogin() { - requestSubscription?.unsubscribe() v?.apply { if (username.text.isEmpty() || password.text.isEmpty()) @@ -40,24 +39,34 @@ class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) { val user = username.text.toString() val pass = password.text.toString() - requestSubscription = service.login(user, pass) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ + scope.launch { + try { + val result = service.login(user, pass) + if (result) { dialog?.dismiss() - context.toast(R.string.login_success) - }, { error -> - login.progress = -1 - login.setText(R.string.unknown_error) - error.message?.let { context.toast(it) } - }) + context.toast(R.string.successfully_logged_in) + } else { + errorResult() + } + } catch (error: Exception) { + errorResult() + error.message?.let { context.toast(it) } + } + } + } + } + + private fun errorResult() { + v?.apply { + login.progress = -1 + login.setText(R.string.unknown_error) } } override fun logout() { if (service.isLogged) { service.logout() - activity?.toast(R.string.logout_success) + activity?.toast(R.string.successfully_logged_out) } } @@ -69,5 +78,4 @@ class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) { interface Listener { fun trackDialogClosed(service: TrackService) } - } diff --git a/app/src/main/res/color/mtrl_btn_bg_selector.xml b/app/src/main/res/color/mtrl_btn_bg_selector.xml new file mode 100644 index 0000000000..a802c7b846 --- /dev/null +++ b/app/src/main/res/color/mtrl_btn_bg_selector.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/primary_button_text_color_selector.xml b/app/src/main/res/color/primary_button_text_color_selector.xml new file mode 100644 index 0000000000..0770047cb9 --- /dev/null +++ b/app/src/main/res/color/primary_button_text_color_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/text_input_stroke.xml b/app/src/main/res/color/text_input_stroke.xml new file mode 100644 index 0000000000..aabd0ef40a --- /dev/null +++ b/app/src/main/res/color/text_input_stroke.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-hdpi/tachiyomi_circle.webp deleted file mode 100644 index 0ec2f7bbe8..0000000000 Binary files a/app/src/main/res/drawable-hdpi/tachiyomi_circle.webp and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-mdpi/tachiyomi_circle.webp deleted file mode 100644 index ab4db0b7b8..0000000000 Binary files a/app/src/main/res/drawable-mdpi/tachiyomi_circle.webp and /dev/null differ diff --git a/app/src/main/res/drawable-night/list_item_selector.xml b/app/src/main/res/drawable-night/list_item_selector.xml deleted file mode 100644 index ff1236a28c..0000000000 --- a/app/src/main/res/drawable-night/list_item_selector.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-xhdpi/card_background.9.png b/app/src/main/res/drawable-xhdpi/card_background.9.png deleted file mode 100644 index 8190c3b276..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/card_background.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-xhdpi/tachiyomi_circle.webp deleted file mode 100644 index 9c2fcfabdd..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/tachiyomi_circle.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.webp deleted file mode 100644 index c5e2b696b5..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/tachiyomi_circle.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/tracker_anilist.webp b/app/src/main/res/drawable-xxxhdpi/ic_tracker_anilist.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/tracker_anilist.webp rename to app/src/main/res/drawable-xxxhdpi/ic_tracker_anilist.webp diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tracker_bangumi.webp b/app/src/main/res/drawable-xxxhdpi/ic_tracker_bangumi.webp new file mode 100644 index 0000000000..26b1d4e963 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tracker_bangumi.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/tracker_kitsu.webp b/app/src/main/res/drawable-xxxhdpi/ic_tracker_kitsu.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/tracker_kitsu.webp rename to app/src/main/res/drawable-xxxhdpi/ic_tracker_kitsu.webp diff --git a/app/src/main/res/drawable-xxxhdpi/tracker_mal.webp b/app/src/main/res/drawable-xxxhdpi/ic_tracker_mal.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/tracker_mal.webp rename to app/src/main/res/drawable-xxxhdpi/ic_tracker_mal.webp diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tracker_shikimori.webp b/app/src/main/res/drawable-xxxhdpi/ic_tracker_shikimori.webp new file mode 100644 index 0000000000..f08ce3839f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tracker_shikimori.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.webp b/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.webp deleted file mode 100644 index 74705a6722..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/tachiyomi_circle.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/tracker_bangumi.webp b/app/src/main/res/drawable-xxxhdpi/tracker_bangumi.webp deleted file mode 100644 index 779ab9af7d..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/tracker_bangumi.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/tracker_shikimori.webp b/app/src/main/res/drawable-xxxhdpi/tracker_shikimori.webp deleted file mode 100644 index d9cf1706fa..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/tracker_shikimori.webp and /dev/null differ diff --git a/app/src/main/res/drawable/action_mode_bg.xml b/app/src/main/res/drawable/action_mode_bg.xml new file mode 100644 index 0000000000..58a1399dde --- /dev/null +++ b/app/src/main/res/drawable/action_mode_bg.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottom_sheet_dialog_fragment.xml b/app/src/main/res/drawable/bg_bottom_sheet_dialog_fragment.xml new file mode 100644 index 0000000000..3a94c7868e --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_sheet_dialog_fragment.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottom_sheet_primary.xml b/app/src/main/res/drawable/bg_bottom_sheet_primary.xml new file mode 100644 index 0000000000..76060c00a3 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_sheet_primary.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/border_circle.xml b/app/src/main/res/drawable/border_circle.xml new file mode 100644 index 0000000000..31b8ae1e95 --- /dev/null +++ b/app/src/main/res/drawable/border_circle.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bordered_list_selector.xml b/app/src/main/res/drawable/bordered_list_selector.xml new file mode 100644 index 0000000000..7f30001a6c --- /dev/null +++ b/app/src/main/res/drawable/bordered_list_selector.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_nav_item_selector.xml b/app/src/main/res/drawable/bottom_nav_item_selector.xml new file mode 100644 index 0000000000..2345ab46ea --- /dev/null +++ b/app/src/main/res/drawable/bottom_nav_item_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_bg_transparent.xml b/app/src/main/res/drawable/button_text_state.xml similarity index 88% rename from app/src/main/res/drawable/button_bg_transparent.xml rename to app/src/main/res/drawable/button_text_state.xml index c23d7a920d..67d93fbdf9 100644 --- a/app/src/main/res/drawable/button_bg_transparent.xml +++ b/app/src/main/res/drawable/button_text_state.xml @@ -3,7 +3,7 @@ - + @@ -19,7 +19,7 @@ - + diff --git a/app/src/main/res/drawable/card_item_selector.xml b/app/src/main/res/drawable/card_item_selector.xml new file mode 100644 index 0000000000..7c810901d6 --- /dev/null +++ b/app/src/main/res/drawable/card_item_selector.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_progress.xml b/app/src/main/res/drawable/circle_progress.xml new file mode 100644 index 0000000000..a0644da5c7 --- /dev/null +++ b/app/src/main/res/drawable/circle_progress.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_rounded_background.xml b/app/src/main/res/drawable/dialog_rounded_background.xml deleted file mode 100644 index 3a54d1729a..0000000000 --- a/app/src/main/res/drawable/dialog_rounded_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/draggable_pill.xml b/app/src/main/res/drawable/draggable_pill.xml new file mode 100644 index 0000000000..8f56150d01 --- /dev/null +++ b/app/src/main/res/drawable/draggable_pill.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroll_background.xml b/app/src/main/res/drawable/fast_scroll_background.xml new file mode 100644 index 0000000000..3f5fb0f8f0 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroll_background.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/filled_circle.xml b/app/src/main/res/drawable/filled_circle.xml new file mode 100644 index 0000000000..9111d830c8 --- /dev/null +++ b/app/src/main/res/drawable/filled_circle.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/full_gradient.xml b/app/src/main/res/drawable/full_gradient.xml new file mode 100644 index 0000000000..f976e3b614 --- /dev/null +++ b/app/src/main/res/drawable/full_gradient.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_shape.xml b/app/src/main/res/drawable/gradient_shape.xml index a15e5c609a..6646bac6cb 100644 --- a/app/src/main/res/drawable/gradient_shape.xml +++ b/app/src/main/res/drawable/gradient_shape.xml @@ -4,7 +4,7 @@ diff --git a/app/src/main/res/drawable/ic_arrow_down_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_down_white_24dp.xml new file mode 100644 index 0000000000..66e132e001 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_white_24dp.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml b/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml index 47d9d1ca02..eea88a322b 100644 --- a/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml +++ b/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml @@ -2,7 +2,8 @@ android:width="32dp" android:height="32dp" android:viewportHeight="32" - android:viewportWidth="32"> + android:viewportWidth="32" + android:tint="?attr/actionBarTintColor"> + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_arrow_up_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_up_white_24dp.xml new file mode 100644 index 0000000000..7f81542ed5 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_white_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_blank_24dp.xml b/app/src/main/res/drawable/ic_blank_24dp.xml new file mode 100644 index 0000000000..fdb1e51ddb --- /dev/null +++ b/app/src/main/res/drawable/ic_blank_24dp.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bookmark_white_24dp.xml b/app/src/main/res/drawable/ic_bookmark_24dp.xml similarity index 92% rename from app/src/main/res/drawable/ic_bookmark_white_24dp.xml rename to app/src/main/res/drawable/ic_bookmark_24dp.xml index a291197bed..38953e0efa 100644 --- a/app/src/main/res/drawable/ic_bookmark_white_24dp.xml +++ b/app/src/main/res/drawable/ic_bookmark_24dp.xml @@ -2,6 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" + android:tint="#FFFFFF" android:viewportHeight="24.0"> - - diff --git a/app/src/main/res/drawable/ic_bookmark_off_24dp.xml b/app/src/main/res/drawable/ic_bookmark_off_24dp.xml new file mode 100644 index 0000000000..419fc05a5b --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_off_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_explore_black_24dp.xml b/app/src/main/res/drawable/ic_browse_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_explore_black_24dp.xml rename to app/src/main/res/drawable/ic_browse_24dp.xml diff --git a/app/src/main/res/drawable/ic_browse_outline_24dp.xml b/app/src/main/res/drawable/ic_browse_outline_24dp.xml new file mode 100644 index 0000000000..5d9214ed20 --- /dev/null +++ b/app/src/main/res/drawable/ic_browse_outline_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_browse_selector_24dp.xml b/app/src/main/res/drawable/ic_browse_selector_24dp.xml new file mode 100644 index 0000000000..ae581550df --- /dev/null +++ b/app/src/main/res/drawable/ic_browse_selector_24dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bug_report_white_24dp.xml b/app/src/main/res/drawable/ic_bug_report_white_24dp.xml new file mode 100644 index 0000000000..a93aba5f4e --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_report_white_24dp.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle_white_24dp.xml b/app/src/main/res/drawable/ic_check_circle_white_24dp.xml new file mode 100644 index 0000000000..7cdc15de00 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_white_24dp.xml b/app/src/main/res/drawable/ic_check_white_24dp.xml new file mode 100644 index 0000000000..17aca2af18 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_white_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_white_24dp.xml index 36b411acea..df58d8e473 100644 --- a/app/src/main/res/drawable/ic_chevron_right_white_24dp.xml +++ b/app/src/main/res/drawable/ic_chevron_right_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_close_white_24dp.xml b/app/src/main/res/drawable/ic_close_white_24dp.xml index 0c8775c4e8..9a35db41e1 100644 --- a/app/src/main/res/drawable/ic_close_white_24dp.xml +++ b/app/src/main/res/drawable/ic_close_white_24dp.xml @@ -1,4 +1,4 @@ - diff --git a/app/src/main/res/drawable/ic_content_copy_white_24dp.xml b/app/src/main/res/drawable/ic_content_copy_white_24dp.xml index 62ad953955..ebf0430c17 100644 --- a/app/src/main/res/drawable/ic_content_copy_white_24dp.xml +++ b/app/src/main/res/drawable/ic_content_copy_white_24dp.xml @@ -1,5 +1,6 @@ - + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_delete_white_24dp.xml b/app/src/main/res/drawable/ic_delete_white_24dp.xml index f9213d2b52..c804ca372d 100644 --- a/app/src/main/res/drawable/ic_delete_white_24dp.xml +++ b/app/src/main/res/drawable/ic_delete_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_done_all_grey_24dp.xml b/app/src/main/res/drawable/ic_done_all_grey_24dp.xml deleted file mode 100644 index 9cbc7fdf95..0000000000 --- a/app/src/main/res/drawable/ic_done_all_grey_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_done_all_white_24dp.xml b/app/src/main/res/drawable/ic_done_all_white_24dp.xml index 2479e86cea..dfec71ca44 100644 --- a/app/src/main/res/drawable/ic_done_all_white_24dp.xml +++ b/app/src/main/res/drawable/ic_done_all_white_24dp.xml @@ -1,5 +1,6 @@ - + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_done_white_18dp.xml b/app/src/main/res/drawable/ic_done_white_18dp.xml deleted file mode 100644 index 3e9103eb03..0000000000 --- a/app/src/main/res/drawable/ic_done_white_18dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_drag_handle_black_24dp.xml b/app/src/main/res/drawable/ic_drag_handle_black_24dp.xml new file mode 100644 index 0000000000..94417b8d78 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle_black_24dp.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_white_24dp.xml b/app/src/main/res/drawable/ic_edit_white_24dp.xml index 46462b5726..82decf34bd 100644 --- a/app/src/main/res/drawable/ic_edit_white_24dp.xml +++ b/app/src/main/res/drawable/ic_edit_white_24dp.xml @@ -1,5 +1,7 @@ - + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml index fd3ce4a468..89a9bc5175 100644 --- a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml +++ b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_extension_black_24dp.xml b/app/src/main/res/drawable/ic_extension_black_24dp.xml deleted file mode 100644 index d3dd094816..0000000000 --- a/app/src/main/res/drawable/ic_extension_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_eye_24dp.xml b/app/src/main/res/drawable/ic_eye_24dp.xml new file mode 100644 index 0000000000..2d40aefe5f --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eye_off_24dp.xml b/app/src/main/res/drawable/ic_eye_off_24dp.xml new file mode 100644 index 0000000000..097850547f --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_off_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_file_download_white_24dp.xml b/app/src/main/res/drawable/ic_file_download_white_24dp.xml index e43b8645a5..7fc108d2f7 100644 --- a/app/src/main/res/drawable/ic_file_download_white_24dp.xml +++ b/app/src/main/res/drawable/ic_file_download_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_filter_list_white_24dp.xml b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml index 7d435fa2b8..c6c62aa87b 100644 --- a/app/src/main/res/drawable/ic_filter_list_white_24dp.xml +++ b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_glasses_black_128dp.xml b/app/src/main/res/drawable/ic_glasses_black_128dp.xml deleted file mode 100644 index fbf52def5a..0000000000 --- a/app/src/main/res/drawable/ic_glasses_black_128dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_heart_24dp.xml b/app/src/main/res/drawable/ic_heart_24dp.xml new file mode 100644 index 0000000000..7f3d547d6e --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_heart_outline_24dp.xml b/app/src/main/res/drawable/ic_heart_outline_24dp.xml new file mode 100644 index 0000000000..c83885db7c --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_outline_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_help_black_24dp.xml b/app/src/main/res/drawable/ic_help_black_24dp.xml index 1517747d07..4e613fd6ee 100644 --- a/app/src/main/res/drawable/ic_help_black_24dp.xml +++ b/app/src/main/res/drawable/ic_help_black_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_history_black_24dp.xml b/app/src/main/res/drawable/ic_history_black_24dp.xml new file mode 100644 index 0000000000..3344c4fcc4 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_black_24dp.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_white_128dp.xml b/app/src/main/res/drawable/ic_history_white_128dp.xml new file mode 100644 index 0000000000..46514a1a00 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_white_128dp.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml new file mode 100644 index 0000000000..f842adddb7 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_label_outline_white_24dp.xml b/app/src/main/res/drawable/ic_label_outline_white_24dp.xml new file mode 100644 index 0000000000..14a4bd7b55 --- /dev/null +++ b/app/src/main/res/drawable/ic_label_outline_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_label_white_24dp.xml b/app/src/main/res/drawable/ic_label_white_24dp.xml index b3c9371449..a242b38e9b 100644 --- a/app/src/main/res/drawable/ic_label_white_24dp.xml +++ b/app/src/main/res/drawable/ic_label_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_library_24dp.xml b/app/src/main/res/drawable/ic_library_24dp.xml new file mode 100644 index 0000000000..49e6c06b96 --- /dev/null +++ b/app/src/main/res/drawable/ic_library_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_library_outline_24dp.xml b/app/src/main/res/drawable/ic_library_outline_24dp.xml new file mode 100644 index 0000000000..916e94524a --- /dev/null +++ b/app/src/main/res/drawable/ic_library_outline_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_library_selector_24dp.xml b/app/src/main/res/drawable/ic_library_selector_24dp.xml new file mode 100644 index 0000000000..25ca378800 --- /dev/null +++ b/app/src/main/res/drawable/ic_library_selector_24dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_lock_white_24dp.xml b/app/src/main/res/drawable/ic_lock_white_24dp.xml new file mode 100644 index 0000000000..146c066b5f --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml b/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml deleted file mode 100644 index da83afdb1b..0000000000 --- a/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_open_in_webview_white_24dp.xml b/app/src/main/res/drawable/ic_open_in_webview_white_24dp.xml new file mode 100644 index 0000000000..cdaef7b1b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_webview_white_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pause_white_24dp.xml b/app/src/main/res/drawable/ic_pause_white_24dp.xml index 8356ff57f5..399328987e 100644 --- a/app/src/main/res/drawable/ic_pause_white_24dp.xml +++ b/app/src/main/res/drawable/ic_pause_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml index 81a8f74f6e..d8581fb2c6 100644 --- a/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml +++ b/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_radio_button_unchecked_white_24dp.xml b/app/src/main/res/drawable/ic_radio_button_unchecked_white_24dp.xml new file mode 100644 index 0000000000..8d1f811530 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_button_unchecked_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_chrome_reader_mode_black_24dp.xml b/app/src/main/res/drawable/ic_read_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_chrome_reader_mode_black_24dp.xml rename to app/src/main/res/drawable/ic_read_24dp.xml diff --git a/app/src/main/res/drawable/ic_recent_read_24dp.xml b/app/src/main/res/drawable/ic_recent_read_24dp.xml new file mode 100644 index 0000000000..11c4d5e625 --- /dev/null +++ b/app/src/main/res/drawable/ic_recent_read_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_recent_read_outline_24dp.xml b/app/src/main/res/drawable/ic_recent_read_outline_24dp.xml new file mode 100644 index 0000000000..15454ddf4f --- /dev/null +++ b/app/src/main/res/drawable/ic_recent_read_outline_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_recent_read_selector_24dp.xml b/app/src/main/res/drawable/ic_recent_read_selector_24dp.xml new file mode 100644 index 0000000000..81ea68a928 --- /dev/null +++ b/app/src/main/res/drawable/ic_recent_read_selector_24dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml index a8175c316a..f7b235c8cf 100644 --- a/app/src/main/res/drawable/ic_refresh_white_24dp.xml +++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_search_white_24dp.xml b/app/src/main/res/drawable/ic_search_white_24dp.xml index 47432c174b..80b1837345 100644 --- a/app/src/main/res/drawable/ic_search_white_24dp.xml +++ b/app/src/main/res/drawable/ic_search_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_select_all_white_24dp.xml b/app/src/main/res/drawable/ic_select_all_white_24dp.xml deleted file mode 100644 index 0fc49c9237..0000000000 --- a/app/src/main/res/drawable/ic_select_all_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml deleted file mode 100644 index ace746c40e..0000000000 --- a/app/src/main/res/drawable/ic_settings_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_white_24dp.xml b/app/src/main/res/drawable/ic_settings_white_24dp.xml index ce997a727d..1e5614c590 100644 --- a/app/src/main/res/drawable/ic_settings_white_24dp.xml +++ b/app/src/main/res/drawable/ic_settings_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_shape_black_128dp.xml b/app/src/main/res/drawable/ic_shape_black_128dp.xml deleted file mode 100644 index 98a101f5ef..0000000000 --- a/app/src/main/res/drawable/ic_shape_black_128dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_share_white_24dp.xml b/app/src/main/res/drawable/ic_share_white_24dp.xml index c5027c6598..63f2305b86 100644 --- a/app/src/main/res/drawable/ic_share_white_24dp.xml +++ b/app/src/main/res/drawable/ic_share_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportHeight="24.0" - android:viewportWidth="24.0"> + android:viewportWidth="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_sort_by_numeric_white_24dp.xml b/app/src/main/res/drawable/ic_sort_by_numeric_white_24dp.xml deleted file mode 100644 index 5bddea0f52..0000000000 --- a/app/src/main/res/drawable/ic_sort_by_numeric_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sort_white_24dp.xml b/app/src/main/res/drawable/ic_sort_white_24dp.xml index a0c153ad01..5e666748a9 100644 --- a/app/src/main/res/drawable/ic_sort_white_24dp.xml +++ b/app/src/main/res/drawable/ic_sort_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_star_12dp.xml b/app/src/main/res/drawable/ic_star_12dp.xml new file mode 100644 index 0000000000..69b488db9d --- /dev/null +++ b/app/src/main/res/drawable/ic_star_12dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_24dp.xml b/app/src/main/res/drawable/ic_star_24dp.xml new file mode 100644 index 0000000000..eacf681ceb --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_start_reading_white_24dp.xml b/app/src/main/res/drawable/ic_start_reading_white_24dp.xml new file mode 100644 index 0000000000..1587b50330 --- /dev/null +++ b/app/src/main/res/drawable/ic_start_reading_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap_calls_white_24dp.xml b/app/src/main/res/drawable/ic_swap_calls_white_24dp.xml new file mode 100644 index 0000000000..141acfc7ce --- /dev/null +++ b/app/src/main/res/drawable/ic_swap_calls_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml index ce8796cb79..885909db8c 100644 --- a/app/src/main/res/drawable/ic_sync_black_24dp.xml +++ b/app/src/main/res/drawable/ic_sync_black_24dp.xml @@ -2,6 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" + android:tint="?actionBarTintColor" android:viewportHeight="24.0"> - - diff --git a/app/src/main/res/drawable/ic_tune_white_24dp.xml b/app/src/main/res/drawable/ic_tune_white_24dp.xml new file mode 100644 index 0000000000..ed6f2630b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_tune_white_24dp.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_update_black_24dp.xml b/app/src/main/res/drawable/ic_update_black_24dp.xml index a1a7cbdfd3..5e6ebfe987 100644 --- a/app/src/main/res/drawable/ic_update_black_24dp.xml +++ b/app/src/main/res/drawable/ic_update_black_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_view_list_white_24dp.xml b/app/src/main/res/drawable/ic_view_list_white_24dp.xml index 222dc3b35b..63fbbb491b 100644 --- a/app/src/main/res/drawable/ic_view_list_white_24dp.xml +++ b/app/src/main/res/drawable/ic_view_list_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/ic_view_module_white_24dp.xml b/app/src/main/res/drawable/ic_view_module_white_24dp.xml index aeb72e7b40..1471bc6608 100644 --- a/app/src/main/res/drawable/ic_view_module_white_24dp.xml +++ b/app/src/main/res/drawable/ic_view_module_white_24dp.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?attr/actionBarTintColor"> diff --git a/app/src/main/res/drawable/library_compact_grid_selector.xml b/app/src/main/res/drawable/library_compact_grid_selector.xml new file mode 100644 index 0000000000..1094038d2e --- /dev/null +++ b/app/src/main/res/drawable/library_compact_grid_selector.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector.xml b/app/src/main/res/drawable/library_item_selector.xml new file mode 100644 index 0000000000..5ef551b915 --- /dev/null +++ b/app/src/main/res/drawable/library_item_selector.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_item_selector.xml b/app/src/main/res/drawable/list_item_selector.xml index d219df8b1c..8f92da27e8 100644 --- a/app/src/main/res/drawable/list_item_selector.xml +++ b/app/src/main/res/drawable/list_item_selector.xml @@ -1,18 +1,21 @@ + android:color="@color/fullRippleColor"> + + + - + - + - - + + diff --git a/app/src/main/res/drawable/list_item_selector_amoled.xml b/app/src/main/res/drawable/list_item_selector_amoled.xml deleted file mode 100644 index e11e4e9e53..0000000000 --- a/app/src/main/res/drawable/list_item_selector_amoled.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/list_item_selector_dark.xml b/app/src/main/res/drawable/list_item_selector_dark.xml index fea6891057..48af7d3484 100644 --- a/app/src/main/res/drawable/list_item_selector_dark.xml +++ b/app/src/main/res/drawable/list_item_selector_dark.xml @@ -7,14 +7,16 @@ - - - - + + + - - - diff --git a/app/src/main/res/drawable/round_clear_border.xml b/app/src/main/res/drawable/round_clear_border.xml new file mode 100644 index 0000000000..d855ff815e --- /dev/null +++ b/app/src/main/res/drawable/round_clear_border.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_play_background.xml b/app/src/main/res/drawable/round_play_background.xml new file mode 100644 index 0000000000..9452dd735c --- /dev/null +++ b/app/src/main/res/drawable/round_play_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_ripple.xml b/app/src/main/res/drawable/round_ripple.xml new file mode 100644 index 0000000000..d4c17b310f --- /dev/null +++ b/app/src/main/res/drawable/round_ripple.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_textview_background.xml b/app/src/main/res/drawable/round_textview_background.xml index 867628b7f3..698ec6ff0e 100644 --- a/app/src/main/res/drawable/round_textview_background.xml +++ b/app/src/main/res/drawable/round_textview_background.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/res/drawable/round_textview_border.xml b/app/src/main/res/drawable/round_textview_border.xml new file mode 100644 index 0000000000..f8aae133d2 --- /dev/null +++ b/app/src/main/res/drawable/round_textview_border.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_ripple.xml b/app/src/main/res/drawable/rounded_ripple.xml new file mode 100644 index 0000000000..9fcec08167 --- /dev/null +++ b/app/src/main/res/drawable/rounded_ripple.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/sc_explore_48dp.xml b/app/src/main/res/drawable/sc_explore_48dp.xml deleted file mode 100644 index 26cd282b1b..0000000000 --- a/app/src/main/res/drawable/sc_explore_48dp.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/sc_book_48dp.xml b/app/src/main/res/drawable/sc_extensions_48dp.xml similarity index 59% rename from app/src/main/res/drawable/sc_book_48dp.xml rename to app/src/main/res/drawable/sc_extensions_48dp.xml index 163ce55484..37377f0095 100644 --- a/app/src/main/res/drawable/sc_book_48dp.xml +++ b/app/src/main/res/drawable/sc_extensions_48dp.xml @@ -14,6 +14,6 @@ android:translateY="12"> + android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.12 11.88,1 10.5,1S8,2.12 8,3.5V5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8H3.5c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-1.5c0,-1.49 1.21,-2.7 2.7,-2.7 1.49,0 2.7,1.21 2.7,2.7V22H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.38,0 2.5,-1.12 2.5,-2.5S21.88,11 20.5,11z"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/sc_glasses_48dp.xml b/app/src/main/res/drawable/sc_glasses_48dp.xml index 8c3a3e28bc..c90cfd147d 100644 --- a/app/src/main/res/drawable/sc_glasses_48dp.xml +++ b/app/src/main/res/drawable/sc_glasses_48dp.xml @@ -14,6 +14,6 @@ android:translateY="12"> + android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_gradient_top_shadow.xml b/app/src/main/res/drawable/shape_gradient_top_shadow.xml new file mode 100644 index 0000000000..e709d2332a --- /dev/null +++ b/app/src/main/res/drawable/shape_gradient_top_shadow.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/square_ripple.xml b/app/src/main/res/drawable/square_ripple.xml new file mode 100644 index 0000000000..6effa62ed9 --- /dev/null +++ b/app/src/main/res/drawable/square_ripple.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/unread_angled_badge.xml b/app/src/main/res/drawable/unread_angled_badge.xml new file mode 100644 index 0000000000..b634e73025 --- /dev/null +++ b/app/src/main/res/drawable/unread_angled_badge.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/manga_info_controller.xml b/app/src/main/res/layout-land/manga_info_controller.xml deleted file mode 100644 index 51eec6280c..0000000000 --- a/app/src/main/res/layout-land/manga_info_controller.xml +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/reader_color_filter_sheet.xml b/app/src/main/res/layout-land/reader_color_filter_sheet.xml index 75c1f8424c..4dc4bd9b0d 100644 --- a/app/src/main/res/layout-land/reader_color_filter_sheet.xml +++ b/app/src/main/res/layout-land/reader_color_filter_sheet.xml @@ -4,18 +4,18 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" + android:background="?android:colorBackground" android:baselineAligned="false" - android:background="?android:colorBackground"> + android:orientation="horizontal"> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/scroll"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/frame"> - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/auto_ext_checkbox.xml b/app/src/main/res/layout/auto_ext_checkbox.xml new file mode 100644 index 0000000000..876882efcd --- /dev/null +++ b/app/src/main/res/layout/auto_ext_checkbox.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_controller.xml b/app/src/main/res/layout/catalogue_controller.xml index e2a0cdb348..76582f5d8e 100644 --- a/app/src/main/res/layout/catalogue_controller.xml +++ b/app/src/main/res/layout/catalogue_controller.xml @@ -2,6 +2,7 @@ @@ -22,5 +23,19 @@ android:visibility="gone"/> + + diff --git a/app/src/main/res/layout/catalogue_drawer.xml b/app/src/main/res/layout/catalogue_drawer.xml deleted file mode 100644 index cb6440ee7a..0000000000 --- a/app/src/main/res/layout/catalogue_drawer.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/layout/catalogue_drawer_content.xml b/app/src/main/res/layout/catalogue_drawer_content.xml index 450078cab4..8936e405ef 100644 --- a/app/src/main/res/layout/catalogue_drawer_content.xml +++ b/app/src/main/res/layout/catalogue_drawer_content.xml @@ -1,70 +1,71 @@ - - + android:layout_height="match_parent" + android:layout_marginTop="?attr/actionBarSize" + android:clipToPadding="false" + android:fitsSystemWindows="true" /> - - - - - - - + android:layout_height="?attr/actionBarSize" + android:layout_gravity="top" + android:background="@drawable/bg_bottom_sheet_primary" + android:backgroundTint="?attr/colorSecondary" + android:clickable="true" + android:elevation="0dp" + android:focusable="true" + android:orientation="horizontal"> - - -