diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0c2dda36b..b77cf7f29 100755 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,17 @@ +1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** +2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4) +3. What is your type of issue? + * [Catalogue request](#catalogue-requests) + * [Bugs](#bugs) + * [Feature requests](#feature-requests) + * [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation) +4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new) + +*** + # Catalogue requests -* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here +* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here # Bugs * Include version (Setting > About > Version) @@ -8,17 +19,9 @@ * Dev version is equal to the number of commits as seen in the main page * Include steps to reproduce (if not obvious from description) * Include screenshot (if needed) -* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed) -* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** +* If it could be device-dependent, try reproducing on another device (if possible) * For large logs use http://pastebin.com/ (or similar) -* For multipart issues **use list** like this: - * [x] Done - * [ ] Not done -``` -* [x] Done -* [ ] Not done -``` -* Don't put together too many unrelated requests into one issue +* Don't group unrelated requests into one issue DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 @@ -28,7 +31,3 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75 * Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does" * Include screenshot (if needed) - -# Translations - -[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 47f813ac5..c2b4ab5a7 100755 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1 @@ **Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting** - -Remove line above and describe your issue here. Fill out version below. Use Preview. - - -Version: r000 or v0.0.0 -(other relevant info like OS) diff --git a/.travis/build.sh b/.travis/build.sh new file mode 100755 index 000000000..d0c376bef --- /dev/null +++ b/.travis/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +git fetch --unshallow #required for commit count + +if [ -z "$TRAVIS_TAG" ]; then + ./gradlew clean assembleStandardDebug + + COMMIT_COUNT=$(git rev-list --count HEAD) + export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk" + + mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT +else + ./gradlew clean assembleStandardRelease + + TOOLS="${ANDROID_HOME}/build-tools/26.0.1" + export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk" + + ${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk + ${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk +fi diff --git a/.travis/deploy.sh b/.travis/deploy.sh new file mode 100755 index 000000000..c95222614 --- /dev/null +++ b/.travis/deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +pattern="tachiyomi-r*" +files=( $pattern ) +export ARTIFACT="${files[0]}" + +if [ -z "$ARTIFACT" ]; then + echo "Artifact not found" + exit 1 +fi + +export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}" + +scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/ +ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest diff --git a/.travis/secrets.tar.enc b/.travis/secrets.tar.enc new file mode 100644 index 000000000..a3fa82b35 Binary files /dev/null and b/.travis/secrets.tar.enc differ diff --git a/README.md b/README.md index 9af91415f..5e4200b75 100755 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ TachiyomiEH is a fork of the [original Tachiyomi app](https://github.com/inorich * Readmangatoday * Mangasee * Wiemanga +* And more! ##### NSFW * E-Hentai diff --git a/app/build.gradle b/app/build.gradle index 1581d1351..160096473 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,12 +3,12 @@ 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' //Realm (EH) apply plugin: 'realm-android' -if (file("custom.gradle").exists()) { - apply from: "custom.gradle" -} +shortcutHelper.filePath = './shortcuts.xml' ext { // Git is needed in your system PATH for these commands to work. @@ -31,8 +31,8 @@ ext { } android { - compileSdkVersion 25 - buildToolsVersion "25.0.2" + compileSdkVersion 26 + buildToolsVersion "26.0.2" publishNonDefault true dexOptions { @@ -42,7 +42,7 @@ android { defaultConfig { applicationId "eu.kanade.tachiyomi.eh2" minSdkVersion 16 - targetSdkVersion 25 + targetSdkVersion 26 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" versionCode 6104 versionName "v6.1.4-EH" @@ -73,15 +73,20 @@ android { } } + flavorDimensions "default" + productFlavors { standard { buildConfigField "boolean", "INCLUDE_UPDATER", "true" + dimension "default" } fdroid { + dimension "default" } dev { minSdkVersion 21 resConfigs "en", "xxhdpi" + dimension "default" } } @@ -103,111 +108,133 @@ android { dependencies { // Modified dependencies - compile 'com.github.inorichi:subsampling-scale-image-view:01e5385' - compile 'com.github.inorichi:tachimage:68cd311' - compile 'com.github.inorichi:junrar-android:634c1f5' + implementation 'com.github.inorichi:subsampling-scale-image-view:c19b883' + implementation 'com.github.inorichi:junrar-android:634c1f5' // Android support library - final support_library_version = '25.4.0' - compile "com.android.support:support-v4:$support_library_version" - compile "com.android.support:appcompat-v7:$support_library_version" - compile "com.android.support:cardview-v7:$support_library_version" - compile "com.android.support:design:$support_library_version" - compile "com.android.support:recyclerview-v7:$support_library_version" - compile "com.android.support:support-annotations:$support_library_version" - compile "com.android.support:customtabs:$support_library_version" + final support_library_version = '26.1.0' + implementation "com.android.support:support-v4:$support_library_version" + implementation "com.android.support:appcompat-v7:$support_library_version" + implementation "com.android.support:cardview-v7:$support_library_version" + implementation "com.android.support:design:$support_library_version" + implementation "com.android.support:recyclerview-v7:$support_library_version" + implementation "com.android.support:preference-v7:$support_library_version" + implementation "com.android.support:support-annotations:$support_library_version" + implementation "com.android.support:customtabs:$support_library_version" - compile 'com.android.support.constraint:constraint-layout:1.0.2' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' - compile 'com.android.support:multidex:1.0.1' + implementation 'com.android.support:multidex:1.0.1' // ReactiveX - compile 'io.reactivex:rxandroid:1.2.1' - compile 'io.reactivex:rxjava:1.3.0' - compile 'com.jakewharton.rxrelay:rxrelay:1.2.0' - compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' - compile 'com.github.pwittchen:reactivenetwork:0.7.0' + implementation 'io.reactivex:rxandroid:1.2.1' + implementation 'io.reactivex:rxjava:1.3.3' + implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' + implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' + implementation 'com.github.pwittchen:reactivenetwork:0.7.0' // Network client - compile "com.squareup.okhttp3:okhttp:3.8.1" - compile 'com.squareup.okio:okio:1.13.0' + implementation "com.squareup.okhttp3:okhttp:3.9.0" + implementation 'com.squareup.okio:okio:1.13.0' // REST final retrofit_version = '2.3.0' - compile "com.squareup.retrofit2:retrofit:$retrofit_version" - compile "com.squareup.retrofit2:converter-gson:$retrofit_version" - compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" // JSON - compile 'com.google.code.gson:gson:2.8.1' - compile 'com.github.salomonbrys.kotson:kotson:2.5.0' + implementation 'com.google.code.gson:gson:2.8.2' + implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' // YAML - compile 'com.github.bmoliveira:snake-yaml:v1.18-android' + implementation 'com.github.bmoliveira:snake-yaml:v1.18-android' // JavaScript engine - compile 'com.squareup.duktape:duktape-android:1.1.0' + implementation 'com.squareup.duktape:duktape-android:1.2.0' // Disk - compile 'com.jakewharton:disklrucache:2.0.2' - compile 'com.github.seven332:unifile:1.0.0' + implementation 'com.jakewharton:disklrucache:2.0.2' + implementation 'com.github.seven332:unifile:1.0.0' // HTML parser - compile 'org.jsoup:jsoup:1.10.2' + implementation 'org.jsoup:jsoup:1.10.2' // Job scheduling - compile 'com.evernote:android-job:1.1.11' - compile 'com.google.android.gms:play-services-gcm:11.0.1' + implementation 'com.evernote:android-job:1.2.0' + implementation 'com.google.android.gms:play-services-gcm:11.6.0' // Changelog - compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' + implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' // Database - compile "com.pushtorefresh.storio:sqlite:1.13.0" + implementation "com.pushtorefresh.storio:sqlite:1.13.0" // Model View Presenter final nucleus_version = '3.0.0' - compile "info.android15.nucleus:nucleus:$nucleus_version" - compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version" + implementation "info.android15.nucleus:nucleus:$nucleus_version" + implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version" // Dependency injection - compile "uy.kohesive.injekt:injekt-core:1.16.1" + implementation "uy.kohesive.injekt:injekt-core:1.16.1" // Image library - compile 'com.github.bumptech.glide:glide:3.8.0' - compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar' + final glide_version = '4.3.1' + 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 - compile 'jp.wasabeef:glide-transformations:2.0.2' + implementation 'jp.wasabeef:glide-transformations:3.0.1' // Logging - compile 'com.jakewharton.timber:timber:4.5.1' + implementation 'com.jakewharton.timber:timber:4.6.0' // Crash reports - compile 'ch.acra:acra:4.9.2' + implementation 'ch.acra:acra:4.9.2' // Sort - compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' + implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' // UI - compile 'com.dmitrymalkovich.android:material-design-dimens:1.4' - compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' - compile 'eu.davidea:flexible-adapter:5.0.0-rc1' - compile 'com.nononsenseapps:filepicker:2.5.2' - compile 'com.github.amulyakhare:TextDrawable:558677e' - compile 'com.afollestad.material-dialogs:core:0.9.4.5' - compile 'me.zhanghai.android.systemuihelper:library:1.0.0' - compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' + 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.0.0-rc3' + implementation 'com.nononsenseapps:filepicker:2.5.2' + implementation 'com.github.amulyakhare:TextDrawable:558677e' + implementation('com.afollestad.material-dialogs:core:0.9.4.7') { + exclude group: "com.android.support", module: "support-v13" + } + implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' + implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' + implementation 'com.github.mthli:Slice:v1.2' // Conductor - compile "com.bluelinelabs:conductor:2.1.4" - compile 'com.github.inorichi:conductor-support-preference:9e36460' + implementation "com.bluelinelabs:conductor:2.1.4" + implementation 'com.github.inorichi:conductor-support-preference:26.0.2' // RxBindings final rxbindings_version = '1.0.1' - compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" - compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" - compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" - compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version" + 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 = '0.19.1' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" //Pin lock view (EXH) compile 'com.andrognito.pinlockview:pinlockview:2.1.0' @@ -221,23 +248,10 @@ dependencies { //RxJava 2 interop for Realm (EXH) compile 'com.lvla.android:rxjava2-interop-kt:0.2.1' - - // Tests - //Paper DB screws up tests - /*testCompile 'junit:junit:4.12' - testCompile 'org.assertj:assertj-core:1.7.1' - testCompile 'org.mockito:mockito-core:1.10.19' - - final robolectric_version = '3.1.4' - testCompile "org.robolectric:robolectric:$robolectric_version" - testCompile "org.robolectric:shadows-multidex:$robolectric_version" - testCompile "org.robolectric:shadows-play-services:$robolectric_version"*/ - - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } buildscript { - ext.kotlin_version = '1.1.3' + ext.kotlin_version = '1.1.51' repositories { mavenCentral() } @@ -250,49 +264,8 @@ repositories { mavenCentral() } -// Workaround to force a support lib version -configurations.all { - resolutionStrategy.eachDependency { details -> - def requested = details.requested - if (requested.group == 'com.android.support') { - if (!requested.name.startsWith("multidex")) { - details.useVersion '25.4.0' - } - } - } -} - -// add support for placeholders in resource files -//https://code.google.com/p/android/issues/detail?id=69224 -def replacePlaceholdersInFile(basePath, fileName, placeholders) { - def file = new File(basePath, fileName); - - if (!file.exists()) { - logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.") - return; - } - - logger.debug("Replacing placeholders in " + file.toString()) - logger.debug("Placeholders: " + placeholders.toString()) - - def content = file.getText('UTF-8') - - placeholders.each { entry -> - content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value) - } - - file.write(content, 'UTF-8') -} - -afterEvaluate { - android.applicationVariants.all { variant -> - variant.outputs.each { output -> - output.processResources.doFirst { - // prepare placeholder map from manifestPlaceholders including applicationId placeholder - def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId] - - replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders) - } - } +kotlin { + experimental { + coroutines 'enable' } } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index eafb570ec..39999f474 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -6,10 +6,14 @@ -keep class com.hippo.image.** { *; } -keep interface com.hippo.image.** { *; } +-dontwarn nucleus.view.NucleusActionBarActivity # Extensions may require methods unused in the core app -keep class org.jsoup.** { *; } -keep class kotlin.** { *; } +-keep class okhttp3.** { *; } +-keep class com.google.gson.** { *; } +-keep class com.github.salomonbrys.kotson.** { *; } # OkHttp -dontwarn okhttp3.** @@ -20,6 +24,7 @@ # Glide specific rules # # https://github.com/bumptech/glide -keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.AppGlideModule -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { **[] $VALUES; public *; diff --git a/app/src/main/res/xml-v25/shortcuts.xml b/app/shortcuts.xml similarity index 88% rename from app/src/main/res/xml-v25/shortcuts.xml rename to app/shortcuts.xml index 574f1d5e1..f0d74789e 100644 --- a/app/src/main/res/xml-v25/shortcuts.xml +++ b/app/shortcuts.xml @@ -9,8 +9,7 @@ android:shortcutShortLabel="@string/label_library"> + android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> + android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> + android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> + android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c715fd93f..fffcac965 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,14 +26,14 @@ android:theme="@style/Theme.Tachiyomi"> + android:launchMode="singleTop"> - - + + + - - 0) { JobRequest.Builder(TAG) .setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) - .setPersisted(true) .setUpdateCurrent(true) .build() .schedule() 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 4179e0f0a..9b3bf8906 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 @@ -182,29 +182,33 @@ class BackupRestoreService : Service() { private fun getRestoreObservable(uri: Uri): Observable> { val startTime = System.currentTimeMillis() - val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) - val json = JsonParser().parse(reader).asJsonObject + return Observable.just(Unit) + .map { + val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) + val json = JsonParser().parse(reader).asJsonObject - // Get parser version - val version = json.get(VERSION)?.asInt ?: 1 + // Get parser version + val version = json.get(VERSION)?.asInt ?: 1 - // Initialize manager - backupManager = BackupManager(this, version) + // Initialize manager + backupManager = BackupManager(this, version) - val mangasJson = json.get(MANGAS).asJsonArray + val mangasJson = json.get(MANGAS).asJsonArray - restoreAmount = mangasJson.size() + 1 // +1 for categories - restoreProgress = 0 - errors.clear() + restoreAmount = mangasJson.size() + 1 // +1 for categories + restoreProgress = 0 + errors.clear() - // Restore categories - json.get(CATEGORIES)?.let { - backupManager.restoreCategories(it.asJsonArray) - restoreProgress += 1 - showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) - } + // Restore categories + json.get(CATEGORIES)?.let { + backupManager.restoreCategories(it.asJsonArray) + restoreProgress += 1 + showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) + } - return Observable.from(mangasJson) + mangasJson + } + .flatMap { Observable.from(it) } .concatMap { val obj = it.asJsonObject val manga = backupManager.parser.fromJson(obj.get(MANGA)) @@ -317,8 +321,8 @@ class BackupRestoreService : Service() { manga } .filter { it.id != null } - .flatMap { manga -> - chapterFetchObservable(source, manga, chapters) + .flatMap { + chapterFetchObservable(source, it, chapters) // Convert to the manga that contains new chapters. .map { manga } } 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 0b970356f..4868dec1e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -44,13 +44,8 @@ class ChapterCache(private val context: Context) { /** Google Json class used for parsing JSON files. */ private val gson: Gson by injectLazy() - /** Parent directory of the cache. Ensure not null and not root directory or fallback - * to internal cache directory. **/ - private val basePath = context.externalCacheDir?.takeIf { it.absolutePath.length > 1 } - ?: context.cacheDir - /** Cache class used for cache management. */ - private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY), + private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), PARAMETER_APP_VERSION, PARAMETER_VALUE_COUNT, PARAMETER_CACHE_SIZE) @@ -86,10 +81,10 @@ class ChapterCache(private val context: Context) { try { // Remove the extension from the file to get the key of the cache - val key = file.substring(0, file.lastIndexOf(".")) + val key = file.substringBeforeLast(".") // Remove file from cache. return diskCache.remove(key) - } catch (e: IOException) { + } catch (e: Exception) { return false } } 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 dee208e0c..4586f8c18 100755 --- 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,7 +20,8 @@ class CoverCache(private val context: Context) { /** * Cache directory used for cache management. */ - private val cacheDir = context.getExternalFilesDir("covers") + private val cacheDir = context.getExternalFilesDir("covers") ?: + File(context.filesDir, "covers").also { it.mkdirs() } /** * Returns the cover from cache. 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 71ca01420..9ad72908f 100755 --- 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 @@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver() { } } -open class MangaGetResolver : DefaultGetResolver() { - - override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply { +interface BaseMangaGetResolver { + fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply { id = cursor.getLong(cursor.getColumnIndex(COL_ID)) source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE)) url = cursor.getString(cursor.getColumnIndex(COL_URL)) @@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver() { } } +open class MangaGetResolver : DefaultGetResolver(), BaseMangaGetResolver { + + override fun mapFromCursor(cursor: Cursor): Manga { + return mapBaseFromCursor(MangaImpl(), cursor) + } +} + class MangaDeleteResolver : DefaultDeleteResolver() { override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() 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 a7fd291b5..d2067abfd 100755 --- 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 @@ -31,10 +31,7 @@ class ChapterImpl : Chapter { if (other == null || javaClass != other.javaClass) return false val chapter = other as Chapter - // Forces updates on manga if scanlator changes. This will allow existing manga in library - // with scanlator to update. - return url == chapter.url && scanlator == chapter.scanlator - + return url == chapter.url } override fun hashCode(): Int { 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 new file mode 100644 index 000000000..b9a7d9428 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt @@ -0,0 +1,9 @@ +package eu.kanade.tachiyomi.data.database.models + +class LibraryManga : MangaImpl() { + + var unread: Int = 0 + + var category: Int = 0 + +} \ No newline at end of file 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 7621f64d8..1782662dc 100755 --- 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 @@ -16,10 +16,6 @@ interface Manga : SManga { var chapter_flags: Int - var unread: Int - - var category: Int - fun setChapterOrder(order: Int) { setFlags(order, SORT_MASK) } 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 401d99a05..977864c24 100755 --- 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 @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.data.database.models -class MangaImpl : Manga { +open class MangaImpl : Manga { override var id: Long? = null @@ -32,10 +32,6 @@ class MangaImpl : Manga { override var chapter_flags: Int = 0 - @Transient override var unread: Int = 0 - - @Transient override var category: Int = 0 - override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false 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 ed5ce5e8b..17348c5b1 100755 --- 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 @@ -4,6 +4,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.Query 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.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver @@ -23,7 +24,7 @@ interface MangaQueries : DbProvider { .prepare() fun getLibraryMangas() = db.get() - .listOfObjects(Manga::class.java) + .listOfObjects(LibraryManga::class.java) .withQuery(RawQuery.builder() .query(libraryQuery) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) 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 4be02fece..77369827a 100755 --- 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 @@ -1,24 +1,23 @@ package eu.kanade.tachiyomi.data.database.resolvers import android.database.Cursor -import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver -import eu.kanade.tachiyomi.data.database.models.Manga +import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver +import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver +import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.tables.MangaTable -class LibraryMangaGetResolver : MangaGetResolver() { +class LibraryMangaGetResolver : DefaultGetResolver(), BaseMangaGetResolver { companion object { val INSTANCE = LibraryMangaGetResolver() } - override fun mapFromCursor(cursor: Cursor): Manga { - val manga = super.mapFromCursor(cursor) + override fun mapFromCursor(cursor: Cursor): LibraryManga { + val manga = LibraryManga() - val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD) - manga.unread = cursor.getInt(unreadColumn) - - val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY) - manga.category = cursor.getInt(categoryColumn) + mapBaseFromCursor(manga, cursor) + manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD)) + manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY)) return manga } 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 ed5b950c2..aeec8e33c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download import android.content.Context import android.graphics.BitmapFactory import android.support.v4.app.NotificationCompat -import eu.kanade.tachiyomi.Constants 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 import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.util.notificationManager import java.util.regex.Pattern @@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) { * Notification builder. */ private val notification by lazy { - NotificationCompat.Builder(context) + NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER) .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) } @@ -69,7 +69,7 @@ internal class DownloadNotifier(private val context: Context) { * * @param id the id of the notification. */ - private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) { + private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) { context.notificationManager.notify(id, build()) } @@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) { * those can only be dismissed by the user. */ fun dismiss() { - context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) + context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER) } /** @@ -262,7 +262,7 @@ internal class DownloadNotifier(private val context: Context) { setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setProgress(0, 0, false) } - notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID) + notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) // Reset download information errorThrown = true 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 0a65f2119..ae2cf2111 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -122,7 +122,7 @@ class DownloadService : Service() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ state -> onNetworkStateChanged(state) - }, { error -> + }, { _ -> toast(R.string.download_queue_error) stopSelf() }) 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 f0aba1a18..7ffe8c4f8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList import eu.kanade.tachiyomi.util.* +import kotlinx.coroutines.experimental.async import okhttp3.Response import rx.Observable import rx.android.schedulers.AndroidSchedulers @@ -90,12 +91,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro @Volatile private var isRunning: Boolean = false init { - Observable.fromCallable { store.restore() } - .map { downloads -> downloads.filter { isDownloadAllowed(it) } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ downloads -> queue.addAll(downloads) - }, { error -> Timber.e(error) }) + launchNow { + val chapters = async { store.restore() } + queue.addAll(chapters.await()) + } } /** @@ -213,61 +212,54 @@ class Downloader(private val context: Context, private val provider: DownloadPro } /** - * Creates a download object for every chapter and adds them to the downloads queue. This method - * must be called in the main thread. + * Creates a download object for every chapter and adds them to the downloads queue. * * @param manga the manga of the chapters to download. * @param chapters the list of chapters to download. */ - fun queueChapters(manga: Manga, chapters: List) { - val source = sourceManager.get(manga.source) as? HttpSource ?: return + fun queueChapters(manga: Manga, chapters: List) = launchUI { + val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI - val chaptersToQueue = chapters - // Avoid downloading chapters with the same name. - .distinctBy { it.name } - // Add chapters to queue from the start. - .sortedByDescending { it.source_order } - // Create a downloader for each one. - .map { Download(source, manga, it) } - // Filter out those already queued or downloaded. - .filter { isDownloadAllowed(it) } + // Called in background thread, the operation can be slow with SAF. + val chaptersWithoutDir = async { + val mangaDir = provider.findMangaDir(source, manga) - // Return if there's nothing to queue. - if (chaptersToQueue.isEmpty()) - return - - queue.addAll(chaptersToQueue) - - // Initialize queue size. - notifier.initialQueueSize = queue.size - - // Initial multi-thread - notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1 - - if (isRunning) { - // Send the list of downloads to the downloader. - downloadsRelay.call(chaptersToQueue) - } else { - // Show initial notification. - notifier.onProgressChange(queue) + chapters + // Avoid downloading chapters with the same name. + .distinctBy { it.name } + // Filter out those already downloaded. + .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } + // Add chapters to queue from the start. + .sortedByDescending { it.source_order } } - } - /** - * Returns true if the given download can be queued and downloaded. - * - * @param download the download to be checked. - */ - private fun isDownloadAllowed(download: Download): Boolean { - // If the chapter is already queued, don't add it again - if (queue.any { it.chapter.id == download.chapter.id }) - return false + // Runs in main thread (synchronization needed). + val chaptersToQueue = chaptersWithoutDir.await() + // Filter out those already enqueued. + .filter { chapter -> queue.none { it.chapter.id == chapter.id } } + // Create a download for each one. + .map { Download(source, manga, it) } - val dir = provider.findChapterDir(download.source, download.manga, download.chapter) - if (dir != null && dir.exists()) - return false + if (chaptersToQueue.isNotEmpty()) { + queue.addAll(chaptersToQueue) - return true + // Initialize queue size. + notifier.initialQueueSize = queue.size + + // Initial multi-thread + notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1 + + if (isRunning) { + // Send the list of downloads to the downloader. + downloadsRelay.call(chaptersToQueue) + } else { + // Show initial notification. + notifier.onProgressChange(queue) + } + + // Start downloader if needed + DownloadService.start(this@Downloader.context) + } } /** @@ -295,7 +287,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro } return pageListObservable - .doOnNext { pages -> + .doOnNext { _ -> // Delete all temporary (unfinished) files tmpDir.listFiles() ?.filter { it.name!!.endsWith(".tmp") } @@ -311,7 +303,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro // Do when page is downloaded. .doOnNext { notifier.onProgressChange(download, queue) } .toList() - .map { pages -> download } + .map { _ -> download } // Do after download completes .doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) } // If the page list threw, it will resume here 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 389dd3822..8e74d8042 100755 --- 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 @@ -66,6 +66,7 @@ class DownloadQueue( val pageStatusSubject = PublishSubject.create() setPagesSubject(download.pages, pageStatusSubject) return@flatMap pageStatusSubject + .onBackpressureBuffer() .filter { it == Page.READY } .map { 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 6e1e06ff4..a795e2e05 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt @@ -1,35 +1,51 @@ -package eu.kanade.tachiyomi.data.glide - -import com.bumptech.glide.Priority -import com.bumptech.glide.load.data.DataFetcher -import java.io.File -import java.io.IOException -import java.io.InputStream - -open class FileFetcher(private val file: File) : DataFetcher { - - private var data: InputStream? = null - - override fun loadData(priority: Priority): InputStream { - data = file.inputStream() - return data!! - } - - override fun cleanup() { - data?.let { data -> - try { - data.close() - } catch (e: IOException) { - // Ignore - } - } - } - - override fun cancel() { - // Do nothing. - } - - override fun getId(): String { - return file.toString() - } +package eu.kanade.tachiyomi.data.glide + +import android.content.ContentValues.TAG +import android.util.Log +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.data.DataFetcher +import java.io.* + +open class FileFetcher(private val file: File) : DataFetcher { + + private var data: InputStream? = null + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + loadFromFile(callback) + } + + protected fun loadFromFile(callback: DataFetcher.DataCallback) { + try { + data = FileInputStream(file) + } catch (e: FileNotFoundException) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to open file", e) + } + callback.onLoadFailed(e) + return + } + + callback.onDataReady(data) + } + + override fun cleanup() { + try { + data?.close() + } catch (e: IOException) { + // Ignored. + } + } + + override fun cancel() { + // Do nothing. + } + + override fun getDataClass(): Class { + return InputStream::class.java + } + + 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 new file mode 100644 index 000000000..5fec42af7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.data.glide + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.data.DataFetcher +import eu.kanade.tachiyomi.data.database.models.Manga +import java.io.File +import java.io.FileNotFoundException +import java.io.InputStream + +/** + * A [DataFetcher] for loading a cover of a library manga. + * It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network + * and copies the result to the cache. + * + * @param networkFetcher the network fetcher for this cover. + * @param manga the manga of the cover to load. + * @param file the file where this cover should be. It may exists or not. + */ +class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher, + private val manga: Manga, + private val file: File) +: FileFetcher(file) { + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + if (!file.exists()) { + networkFetcher.loadData(priority, object : DataFetcher.DataCallback { + override fun onDataReady(data: InputStream?) { + if (data != null) { + val tmpFile = File(file.path + ".tmp") + try { + // Retrieve destination stream, create parent folders if needed. + val output = try { + tmpFile.outputStream() + } catch (e: FileNotFoundException) { + tmpFile.parentFile.mkdirs() + tmpFile.outputStream() + } + + // Copy the file and rename to the original. + data.use { output.use { data.copyTo(output) } } + tmpFile.renameTo(file) + loadFromFile(callback) + } catch (e: Exception) { + tmpFile.delete() + callback.onLoadFailed(e) + } + } else { + callback.onLoadFailed(Exception("Null data")) + } + } + + override fun onLoadFailed(e: Exception) { + callback.onLoadFailed(e) + } + + }) + } else { + loadFromFile(callback) + } + } + + override fun cleanup() { + super.cleanup() + networkFetcher.cleanup() + } + + override fun cancel() { + super.cancel() + networkFetcher.cancel() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaFileFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaFileFetcher.kt deleted file mode 100755 index 5e594e496..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaFileFetcher.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.data.glide - -import eu.kanade.tachiyomi.data.database.models.Manga -import java.io.File - -open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) { - - /** - * Returns the id for this manga's cover. - * - * Appending the file's modified date to the url, we can force Glide to skip its memory and disk - * lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when - * the file has changed. If the file doesn't exist it will append a 0. - */ - override fun getId(): String { - return manga.thumbnail_url + file.lastModified() - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt index f5342c451..682a2b39c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt @@ -1,23 +1,24 @@ package eu.kanade.tachiyomi.data.glide -import android.content.Context import android.util.LruCache -import com.bumptech.glide.Glide import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher -import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.Options import com.bumptech.glide.load.model.* -import com.bumptech.glide.load.model.stream.StreamModelLoader import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.io.InputStream + /** * A class for loading a cover associated with a [Manga] that can be present in our own cache. - * Coupled with [MangaUrlFetcher], this class allows to implement the following flow: + * Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow: * * - Check in RAM LRU. * - Check in disk LRU. @@ -26,7 +27,7 @@ import java.io.InputStream * * @param context the application context. */ -class MangaModelLoader(context: Context) : StreamModelLoader { +class MangaModelLoader : ModelLoader { /** * Cover cache where persistent covers are stored. @@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader { private val sourceManager: SourceManager by injectLazy() /** - * Base network loader. + * Default network client. */ - private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java, - InputStream::class.java, context) + private val defaultClient = Injekt.get().client /** * LRU cache whose key is the thumbnail url of the manga, and the value contains the request url * and the file where it should be stored in case the manga is a favorite. */ - private val lruCache = LruCache>(100) + private val lruCache = LruCache(100) /** * Map where request headers are stored for a source. @@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader { */ class Factory : ModelLoaderFactory { - override fun build(context: Context, factories: GenericLoaderFactory) - = MangaModelLoader(context) + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return MangaModelLoader() + } override fun teardown() {} } + override fun handles(model: Manga): Boolean { + return true + } + /** * Returns a fetcher for the given manga or null if the url is empty. * @@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader { * @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 getResourceFetcher(manga: Manga, - width: Int, - height: Int): DataFetcher? { - + 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()) { @@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader { if (url.startsWith("http")) { val source = sourceManager.get(manga.source) as? HttpSource - - // Obtain the request url and the file for this url from the LRU cache, or calculate it - // and add them to the cache. - val (glideUrl, file) = lruCache.get(url) ?: - Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply { - lruCache.put(url, this) - } + val glideUrl = GlideUrl(url, getHeaders(manga, source)) // Get the resource fetcher for this request url. - val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) } - ?: baseUrlLoader.getResourceFetcher(glideUrl, width, height) + val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl) + + if (!manga.favorite) { + return ModelLoader.LoadData(glideUrl, networkFetcher) + } + + // Obtain the file for this url from the LRU cache, or retrieve and add it to the cache. + val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) } + + val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file) // Return an instance of the fetcher providing the needed elements. - return MangaUrlFetcher(networkFetcher, file, manga) + 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://")) // Return an instance of the fetcher providing the needed elements. - return MangaFileFetcher(file, manga) + return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file)) } } @@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader { } } + private inline fun LruCache.getOrPut(key: K, defaultValue: () -> V): V { + val value = get(key) + return if (value == null) { + val answer = defaultValue() + put(key, answer) + answer + } else { + 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 new file mode 100644 index 000000000..aa3ebf6f9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.data.glide + +import com.bumptech.glide.load.Key +import eu.kanade.tachiyomi.data.database.models.Manga +import java.io.File +import java.security.MessageDigest + +class MangaSignature(manga: Manga, file: File) : Key { + + private val key = manga.thumbnail_url + file.lastModified() + + override fun equals(other: Any?): Boolean { + return if (other is MangaSignature) { + key == other.key + } else { + false + } + } + + override fun hashCode(): Int { + return key.hashCode() + } + + 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/MangaUrlFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaUrlFetcher.kt deleted file mode 100755 index 193309583..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaUrlFetcher.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.data.glide - -import com.bumptech.glide.Priority -import com.bumptech.glide.load.data.DataFetcher -import eu.kanade.tachiyomi.data.database.models.Manga -import java.io.File -import java.io.FileNotFoundException -import java.io.InputStream - -/** - * A [DataFetcher] for loading a cover of a manga depending on its favorite status. - * If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it - * fallbacks to network and copies it to the cache. - * If the manga is not favorite, it tries to delete the cover from our cache and always fallback - * to network for fetching. - * - * @param networkFetcher the network fetcher for this cover. - * @param file the file where this cover should be. It may exists or not. - * @param manga the manga of the cover to load. - */ -class MangaUrlFetcher(private val networkFetcher: DataFetcher, - private val file: File, - private val manga: Manga) -: MangaFileFetcher(file, manga) { - - override fun loadData(priority: Priority): InputStream { - if (manga.favorite) { - synchronized(file) { - if (!file.exists()) { - val tmpFile = File(file.path + ".tmp") - try { - // Retrieve source stream. - val input = networkFetcher.loadData(priority) - ?: throw Exception("Couldn't open source stream") - - // Retrieve destination stream, create parent folders if needed. - val output = try { - tmpFile.outputStream() - } catch (e: FileNotFoundException) { - tmpFile.parentFile.mkdirs() - tmpFile.outputStream() - } - - // Copy the file and rename to the original. - input.use { output.use { input.copyTo(output) } } - tmpFile.renameTo(file) - } catch (e: Exception) { - tmpFile.delete() - throw e - } - } - } - return super.loadData(priority) - } else { - if (file.exists()) { - file.delete() - } - return networkFetcher.loadData(priority) - } - } - - override fun cancel() { - networkFetcher.cancel() - } - - override fun cleanup() { - super.cleanup() - networkFetcher.cleanup() - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt similarity index 50% rename from app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt index b1b722acb..457f8d228 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt @@ -1,12 +1,18 @@ package eu.kanade.tachiyomi.data.glide import android.content.Context +import android.graphics.drawable.Drawable import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader +import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.module.GlideModule +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 import eu.kanade.tachiyomi.network.NetworkHelper import uy.kohesive.injekt.Injekt @@ -16,17 +22,20 @@ import java.io.InputStream /** * Class used to update Glide module settings */ -class AppGlideModule : GlideModule { +@GlideModule +class TachiGlideModule : AppGlideModule() { override fun applyOptions(context: Context, builder: GlideBuilder) { - // Set the cache size of Glide to 15 MiB - builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024)) + 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) { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { val networkFactory = OkHttpUrlLoader.Factory(Injekt.get().client) - glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory) - glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) + registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) + registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index d109a6f50..ab386b133 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() { .setRequiredNetworkType(wifiRestriction) .setRequiresCharging(acRestriction) .setRequirementsEnforced(true) - .setPersisted(true) .setUpdateCurrent(true) .build() .schedule() 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 4d30b38af..50a19ef4d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -10,16 +10,17 @@ import android.os.Build import android.os.IBinder import android.os.PowerManager import android.support.v4.app.NotificationCompat -import eu.kanade.tachiyomi.Constants 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.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.notification.NotificationReceiver +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.data.track.TrackManager @@ -80,10 +81,12 @@ class LibraryUpdateService( /** * Cached progress notification to avoid creating a lot. */ - private val progressNotification by lazy { NotificationCompat.Builder(this) + 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) .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) } @@ -132,7 +135,11 @@ class LibraryUpdateService( putExtra(KEY_TARGET, target) category?.let { putExtra(KEY_CATEGORY, it.id) } } - context.startService(intent) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + context.startService(intent) + } else { + context.startForegroundService(intent) + } } } @@ -153,6 +160,7 @@ class LibraryUpdateService( */ override fun onCreate() { super.onCreate() + startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build()) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") wakeLock.acquire() @@ -224,7 +232,7 @@ class LibraryUpdateService( * @param target the target to update. * @return a list of manga to update */ - fun getMangaToUpdate(intent: Intent, target: Target): List { + fun getMangaToUpdate(intent: Intent, target: Target): List { val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) var listToUpdate = if (categoryId != -1) @@ -255,7 +263,7 @@ class LibraryUpdateService( * @param mangaToUpdate the list to update * @return an observable delivering the progress of each update. */ - fun updateChapterList(mangaToUpdate: List): Observable { + fun updateChapterList(mangaToUpdate: List): Observable { // Initialize the variables holding the progress of the updates. val count = AtomicInteger(0) // List containing new updates @@ -279,7 +287,7 @@ class LibraryUpdateService( // If there's any error, return empty update and continue. .onErrorReturn { failedUpdates.add(manga) - Pair(emptyList(), emptyList()) + Pair(emptyList(), emptyList()) } // Filter out mangas without new chapters (or failed). .filter { pair -> pair.first.isNotEmpty() } @@ -347,7 +355,7 @@ class LibraryUpdateService( * @param mangaToUpdate the list to update * @return an observable delivering the progress of each update. */ - fun updateDetails(mangaToUpdate: List): Observable { + fun updateDetails(mangaToUpdate: List): Observable { // Initialize the variables holding the progress of the updates. val count = AtomicInteger(0) @@ -358,7 +366,7 @@ class LibraryUpdateService( // Update the details of the manga. .concatMap { manga -> val source = sourceManager.get(manga.source) as? HttpSource - ?: return@concatMap Observable.empty() + ?: return@concatMap Observable.empty() source.fetchMangaDetails(manga) .map { networkManga -> @@ -377,7 +385,7 @@ class LibraryUpdateService( * 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 fun updateTrackings(mangaToUpdate: List): Observable { // Initialize the variables holding the progress of the updates. var count = 0 @@ -417,7 +425,7 @@ class LibraryUpdateService( * @param total the total progress. */ private fun showProgressNotification(manga: Manga, current: Int, total: Int) { - notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification + notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification .setContentTitle(manga.title) .setProgress(total, current, false) .build()) @@ -434,7 +442,7 @@ class LibraryUpdateService( // Append new chapters from a previous, existing notification if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val previousNotification = notificationManager.activeNotifications - .find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID } + .find { it.id == Notifications.ID_LIBRARY_RESULT } if (previousNotification != null) { val oldUpdates = previousNotification.notification.extras @@ -446,7 +454,7 @@ class LibraryUpdateService( } } - notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification { + notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) { setSmallIcon(R.drawable.ic_book_white_24dp) setLargeIcon(notificationBitmap) setContentTitle(getString(R.string.notification_new_chapters)) @@ -466,7 +474,7 @@ class LibraryUpdateService( * Cancels the progress notification. */ private fun cancelProgressNotification() { - notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) + notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS) } /** 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 17767d322..05d1ad6b6 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -5,7 +5,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Handler -import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter @@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() { ACTION_RESUME_DOWNLOADS -> DownloadService.start(context) // Clear the download queue ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) + // Show message notification created + ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created) // Launch share activity and dismiss notification ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) @@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() { ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) // Cancel library update and dismiss notification - ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) + ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS) // Open reader activity ACTION_OPEN_CHAPTER -> { openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), @@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() { // Called to clear downloads. private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" + // Called to notify user shortcut is created. + private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED" + // Called to dismiss notification. private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION" @@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() { return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } + internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_SHORTCUT_CREATED + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + /** * Returns [PendingIntent] that starts a service which dismissed the notification * 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 new file mode 100644 index 000000000..be537dd14 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -0,0 +1,54 @@ +package eu.kanade.tachiyomi.data.notification + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.notificationManager + +/** + * Class to manage the basic information of all the notifications used in the app. + */ +object Notifications { + + /** + * Common notification channel and ids used anywhere. + */ + const val CHANNEL_COMMON = "common_channel" + const val ID_UPDATER = 1 + const val ID_DOWNLOAD_IMAGE = 2 + + /** + * Notification channel and ids used by the library updater. + */ + const val CHANNEL_LIBRARY = "library_channel" + const val ID_LIBRARY_PROGRESS = 101 + const val ID_LIBRARY_RESULT = 102 + + /** + * Notification channel and ids used by the downloader. + */ + const val CHANNEL_DOWNLOADER = "downloader_channel" + const val ID_DOWNLOAD_CHAPTER = 201 + const val ID_DOWNLOAD_CHAPTER_ERROR = 202 + + /** + * Creates the notification channels introduced in Android Oreo. + * + * @param context The application context. + */ + 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), + NotificationManager.IMPORTANCE_LOW), + NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), + NotificationManager.IMPORTANCE_LOW) + ) + context.notificationManager.createNotificationChannels(channels) + } +} \ No newline at end of file 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 2ee14b3bb..5e677eacf 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -105,6 +105,8 @@ object PreferenceKeys { const val defaultCategory = "default_category" + const val downloadBadge = "display_download_badge" + fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" 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 bb51eb850..772d16d15 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -142,6 +142,8 @@ class PreferencesHelper(val context: Context) { fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) + fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false) + fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false) fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt index e3dcb8b84..696de5277 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt @@ -6,8 +6,8 @@ import android.support.v4.app.NotificationCompat import com.evernote.android.job.Job import com.evernote.android.job.JobManager import com.evernote.android.job.JobRequest -import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.notificationManager class UpdateCheckerJob : Job() { @@ -23,7 +23,7 @@ class UpdateCheckerJob : Job() { putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) } - NotificationCompat.Builder(context).update { + 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) @@ -43,7 +43,7 @@ class UpdateCheckerJob : Job() { fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { block() - context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) + context.notificationManager.notify(Notifications.ID_UPDATER, build()) } companion object { @@ -54,7 +54,6 @@ class UpdateCheckerJob : Job() { .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequirementsEnforced(true) - .setPersisted(true) .setUpdateCurrent(true) .build() .schedule() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt index 5174c204f..79190495b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt @@ -4,10 +4,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.support.v4.app.NotificationCompat -import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.notificationManager import java.io.File import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID @@ -49,7 +49,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive /** * Notification shown to user */ - private val notification = NotificationCompat.Builder(context) + private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON) override fun onReceive(context: Context, intent: Intent) { when (intent.getStringExtra(EXTRA_ACTION)) { @@ -82,6 +82,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive private fun updateProgress(progress: Int) { with(notification) { setProgress(100, progress, false) + setOnlyAlertOnce(true) } notification.show() } @@ -96,6 +97,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive with(notification) { setContentText(context.getString(R.string.update_check_notification_download_complete)) setSmallIcon(android.R.drawable.stat_sys_download_done) + setOnlyAlertOnce(false) setProgress(0, 0, false) // Install action setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path))) @@ -105,7 +107,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive // Cancel action addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel), - NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) + NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) } notification.show() } @@ -120,6 +122,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive with(notification) { setContentText(context.getString(R.string.update_check_notification_download_error)) setSmallIcon(android.R.drawable.stat_sys_warning) + setOnlyAlertOnce(false) setProgress(0, 0, false) // Retry action addAction(R.drawable.ic_refresh_grey_24dp_img, @@ -128,7 +131,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive // Cancel action addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel), - NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) + NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) } notification.show() } @@ -138,7 +141,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive * * @param id the id of the notification. */ - private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) { + private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) { context.notificationManager.notify(id, build()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt b/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt index 766bb49a2..c7154deca 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt @@ -17,7 +17,7 @@ class PersistentCookieStore(context: Context) { val cookies = value as? Set if (cookies != null) { try { - val url = HttpUrl.parse("http://$key") + val url = HttpUrl.parse("http://$key") ?: continue val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) } .filter { !it.hasExpired() } cookieMap.put(key, nonExpiredCookies) 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 00662ceb9..d46370e7c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -56,7 +56,7 @@ class LocalSource(private val context: Context) : CatalogueSource { } override val id = ID - override val name = "LocalSource" + override val name = context.getString(R.string.local_source) override val lang = "" override val supportsLatest = true @@ -76,13 +76,13 @@ class LocalSource(private val context: Context) : CatalogueSource { val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state when (state?.index) { 0 -> { - if (state!!.ascending) + if (state.ascending) mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } else mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } } 1 -> { - if (state!!.ascending) + if (state.ascending) mangaDirs = mangaDirs.sortedBy(File::lastModified) else mangaDirs = mangaDirs.sortedByDescending(File::lastModified) @@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource { } else { chapterFile.nameWithoutExtension } - val chapNameCut = chapName.replace(manga.title, "", 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) 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 618684d11..4388439a3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -28,7 +28,11 @@ class Page( @Transient private var statusSubject: Subject? = null override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - progress = (100 * bytesRead / contentLength).toInt() + progress = if (contentLength > 0) { + (100 * bytesRead / contentLength).toInt() + } else { + -1 + } } fun setStatusSubject(subject: Subject?) { 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 5698a3dfd..7b57d10c1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -13,6 +13,7 @@ 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 @@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource { override val id by lazy { val key = "${name.toLowerCase()}/$lang/$versionId" val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) - (0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE + (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE } /** @@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource { /** * Returns an observable with the updated chapter list for a manga. Normally it's not needed to - * override this method. + * override this method. If a manga is licensed an empty chapter list observable is returned * * @param manga the manga to look for chapters. */ override fun fetchChapterList(manga: SManga): Observable> { - return client.newCall(chapterListRequest(manga)) - .asObservableSuccess() - .map { response -> - chapterListParse(response) - } + if (manga.status != SManga.LICENSED) { + return client.newCall(chapterListRequest(manga)) + .asObservableSuccess() + .map { response -> + chapterListParse(response) + } + } else { + return Observable.error(Exception("Licensed - No chapters to show")) + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt index b77adeca3..567174891 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt @@ -11,6 +11,7 @@ import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import timber.log.Timber import java.text.SimpleDateFormat import java.util.regex.Pattern @@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() { val manga = SManga.create() element.select("td a:eq(0)").first().let { manga.setUrlWithoutDomain(it.attr("href")) - manga.title = it.text() + val title = it.text() + //check if cloudfire email obfuscation is affecting title name + if (title.contains("[email protected]", true)) { + try { + var str: String = it.html() + //get the number + str = str.substringAfter("data-cfemail=\"") + str = str.substringBefore("\">[email") + val sb = StringBuilder() + //convert number to char + val r = Integer.valueOf(str.substring(0, 2), 16)!! + var i = 2 + while (i < str.length) { + val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar() + sb.append(c) + i += 2 + } + //replace the new word into the title + manga.title = title.replace("[email protected]", sb.toString(), true) + } catch (e: Exception) { + //on error just default to obfuscated title + Timber.e("error parsing [email protected]", e) + manga.title = title + } + } else { + manga.title = title + } } return manga } @@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() { Genre("Mystery"), Genre("One shot"), Genre("Psychological"), + Genre("Reincarnation"), Genre("Romance"), Genre("School Life"), Genre("Sci-fi"), @@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() { Genre("Smut"), Genre("Sports"), Genre("Supernatural"), + Genre("Time Travel"), Genre("Tragedy"), + Genre("Transported"), Genre("Webtoon"), Genre("Yaoi"), Genre("Yuri") diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt index 4444d9105..cfabaed74 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt @@ -61,7 +61,7 @@ class Mangafox : ParsedHttpSource() { is Status -> url.addQueryParameter(filter.id, filter.state.toString()) is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) } is TextField -> url.addQueryParameter(filter.key, filter.state) - is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString()) + is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString()) is OrderBy -> { url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index]) url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za") @@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() { val infoElement = document.select("div#title").first() val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() val sideInfoElement = document.select("#series_info").first() + val licensedElement = document.select("div.warning").first() val manga = SManga.create() manga.author = rowElement.select("td:eq(1)").first()?.text() manga.artist = rowElement.select("td:eq(2)").first()?.text() manga.genre = rowElement.select("td:eq(3)").first()?.text() manga.description = infoElement.select("p.summary").first()?.text() - manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } + val isLicensed = licensedElement?.text()?.contains("licensed") + if (isLicensed == true) { + manga.status = SManga.LICENSED + } else { + manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } + } + manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") return manga } @@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() { val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) - chapter.name = urlElement.text() + chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text() chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 return chapter } @@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() { private class OrderBy : Filter.Sort("Order by", arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), Filter.Sort.Selection(2, false)) + private class GenreList(genres: List) : Filter.Group("Genres", genres) override fun getFilterList() = FilterList( diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt index 42a8e17b9..befa37973 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt @@ -7,9 +7,13 @@ import okhttp3.HttpUrl import okhttp3.Request import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import java.security.SecureRandom +import java.security.cert.X509Certificate import java.text.ParseException import java.text.SimpleDateFormat import java.util.* +import javax.net.ssl.SSLContext +import javax.net.ssl.X509TrustManager class Mangahere : ParsedHttpSource() { @@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() { override val supportsLatest = true + private val trustManager = object : X509TrustManager { + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + + override fun checkClientTrusted(chain: Array, authType: String) { + } + + override fun checkServerTrusted(chain: Array, authType: String) { + } + } + + private val sslContext = SSLContext.getInstance("SSL").apply { + init(null, arrayOf(trustManager), SecureRandom()) + } + + override val client = super.client.newBuilder() + .sslSocketFactory(sslContext.socketFactory, trustManager) + .build() + override fun popularMangaSelector() = "div.directory_list > ul > li" override fun latestUpdatesSelector() = "div.directory_list > ul > li" @@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() { val infoElement = detailElement.select(".detail_topText").first() val manga = SManga.create() - manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text() - manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text() + manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text() + manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text() manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } @@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() { val pages = mutableListOf() document.select("select.wid60").first()?.getElementsByTag("option")?.forEach { - pages.add(Page(pages.size, it.attr("value"))) + if (!it.attr("value").contains("featured.html")) { + pages.add(Page(pages.size, "http:" + it.attr("value"))) + } } pages.getOrNull(0)?.imageUrl = imageUrlParse(document) return pages @@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() { private class OrderBy : Filter.Sort("Order by", arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), Filter.Sort.Selection(2, false)) + private class GenreList(genres: List) : Filter.Group("Genres", genres) override fun getFilterList() = FilterList( diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt index c4c476a04..79eaa1308 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt @@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() { override val name = "ReadMangaToday" - override val baseUrl = "http://www.readmanga.today" + override val baseUrl = "http://www.readmng.com/" override val lang = "en" @@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() { return pages } - override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") + override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src") private class Status : Filter.TriState("Completed") private class Genre(name: String, val id: Int) : Filter.TriState(name) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt index a104f1b37..20bf46c00 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt @@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() { override val supportsLatest = true - override fun popularMangaRequest(page: Int): Request { - return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) - } + override fun popularMangaRequest(page: Int): Request = + GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { var pageNum = 1 @@ -48,9 +47,7 @@ class Mangachan : ParsedHttpSource() { return GET(url, headers) } - override fun latestUpdatesRequest(page: Int): Request { - return GET("$baseUrl/newestch?page=$page") - } + override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page") override fun popularMangaSelector() = "div.content_row" @@ -76,9 +73,7 @@ class Mangachan : ParsedHttpSource() { return manga } - override fun searchMangaFromElement(element: Element): SManga { - return popularMangaFromElement(element) - } + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) override fun popularMangaNextPageSelector() = "a:contains(Вперед)" @@ -125,16 +120,14 @@ class Mangachan : ParsedHttpSource() { manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) manga.description = descElement.textNodes().first().text() - manga.thumbnail_url = baseUrl + imgElement.attr("src") + manga.thumbnail_url = imgElement.attr("src") return manga } - private fun parseStatus(element: String): Int { - when { - element.contains("перевод завершен") -> return SManga.COMPLETED - element.contains("перевод продолжается") -> return SManga.ONGOING - else -> return SManga.UNKNOWN - } + private fun parseStatus(element: String): Int = when { + element.contains("перевод завершен") -> SManga.COMPLETED + element.contains("перевод продолжается") -> SManga.ONGOING + else -> SManga.UNKNOWN } override fun chapterListSelector() = "table.table_cha tr:gt(1)" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt index b86102356..dbaa49fd7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt @@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() { override val supportsLatest = true - override fun popularMangaRequest(page: Int): Request { - return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) - } + override fun popularMangaRequest(page: Int): Request = + GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) - override fun latestUpdatesRequest(page: Int): Request { - return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) - } + override fun latestUpdatesRequest(page: Int): Request = + GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) override fun popularMangaSelector() = "div.desc" @@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() { return manga } - override fun latestUpdatesFromElement(element: Element): SManga { - return popularMangaFromElement(element) - } + override fun latestUpdatesFromElement(element: Element): SManga = + popularMangaFromElement(element) override fun popularMangaNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val genres = filters.filterIsInstance().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") - return GET("$baseUrl/search?q=$query&$genres", headers) + val genres = filters.filterIsInstance().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] } + return GET("$baseUrl/search/advanced?q=$query&$genres", headers) } override fun searchMangaSelector() = popularMangaSelector() - override fun searchMangaFromElement(element: Element): SManga { - return popularMangaFromElement(element) - } + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) // max 200 results override fun searchMangaNextPageSelector() = null @@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() { return manga } - private fun parseStatus(element: String): Int { - when { - element.contains("

Запрещена публикация произведения по копирайту

") -> return SManga.LICENSED - element.contains("

Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED - element.contains("Перевод: продолжается") -> return SManga.ONGOING - else -> return SManga.UNKNOWN - } + private fun parseStatus(element: String): Int = when { + element.contains("

Запрещена публикация произведения по копирайту

") -> SManga.LICENSED + element.contains("

Сингл") || element.contains("Перевод: завершен") -> SManga.COMPLETED + element.contains("Перевод: продолжается") -> SManga.ONGOING + else -> SManga.UNKNOWN } override fun chapterListSelector() = "div.chapters-link tbody tr" @@ -149,7 +142,7 @@ class Mintmanga : ParsedHttpSource() { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') - * on http://mintmanga.com/search + * on http://mintmanga.com/search/advanced */ override fun getFilterList() = FilterList( Genre("арт", "el_2220"), @@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() { Genre("меха", "el_1318"), Genre("мистика", "el_1324"), Genre("научная фантастика", "el_1325"), + Genre("омегаверс", "el_5676"), Genre("повседневность", "el_1327"), Genre("постапокалиптика", "el_1342"), Genre("приключения", "el_1322"), diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt index bb3af34dc..4bc3383d6 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt @@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() { override fun latestUpdatesSelector() = "div.desc" - override fun popularMangaRequest(page: Int): Request { - return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) - } + override fun popularMangaRequest(page: Int): Request = + GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) - override fun latestUpdatesRequest(page: Int): Request { - return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) - } + override fun latestUpdatesRequest(page: Int): Request = + GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) override fun popularMangaFromElement(element: Element): SManga { val manga = SManga.create() @@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() { return manga } - override fun latestUpdatesFromElement(element: Element): SManga { - return popularMangaFromElement(element) - } + override fun latestUpdatesFromElement(element: Element): SManga = + popularMangaFromElement(element) override fun popularMangaNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val genres = filters.filterIsInstance().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") - return GET("$baseUrl/search?q=$query&$genres", headers) + val genres = filters.filterIsInstance().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] } + return GET("$baseUrl/search/advanced?q=$query&$genres", headers) } override fun searchMangaSelector() = popularMangaSelector() - override fun searchMangaFromElement(element: Element): SManga { - return popularMangaFromElement(element) - } + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) // max 200 results override fun searchMangaNextPageSelector() = null @@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() { return manga } - private fun parseStatus(element: String): Int { - when { - element.contains("

Запрещена публикация произведения по копирайту

") -> return SManga.LICENSED - element.contains("

Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED - element.contains("Перевод: продолжается") -> return SManga.ONGOING - else -> return SManga.UNKNOWN - } + private fun parseStatus(element: String): Int = when { + element.contains("

Запрещена публикация произведения по копирайту

") -> SManga.LICENSED + element.contains("

Сингл") || element.contains("Перевод: завершен") -> SManga.COMPLETED + element.contains("Перевод: продолжается") -> SManga.ONGOING + else -> SManga.UNKNOWN } override fun chapterListSelector() = "div.chapters-link tbody tr" @@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') - * on http://readmanga.me/search + * on http://readmanga.me/search/advanced */ override fun getFilterList() = FilterList( Genre("арт", "el_5685"), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt index 9f55cd033..8ab55d9f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.base.controller import android.os.Bundle -import android.support.v4.view.MenuItemCompat import android.support.v7.app.AppCompatActivity import android.view.LayoutInflater import android.view.MenuItem @@ -34,7 +33,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr return null } - private fun setTitle() { + fun setTitle() { var parentController = parentController while (parentController != null) { if (parentController is BaseController && parentController.getTitle() != null) { @@ -52,7 +51,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr * Issue link: https://issuetracker.google.com/issues/37657375 */ fun MenuItem.fixExpand() { - val expandListener = object : MenuItemCompat.OnActionExpandListener { + setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem): Boolean { return true } @@ -61,8 +60,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr activity?.invalidateOptionsMenu() return true } - } - MenuItemCompat.setOnActionExpandListener(this, expandListener) + }) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt index 63eba25ed..3f252409c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt @@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory import nucleus.presenter.Presenter @Suppress("LeakingThis") -abstract class NucleusController

>(val bundle: Bundle? = null) : RxController(), +abstract class NucleusController

>(val bundle: Bundle? = null) : RxController(bundle), PresenterFactory

{ private val delegate = NucleusConductorDelegate(this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java index 62a50af83..ddc4aba5a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java @@ -10,7 +10,6 @@ public class NucleusConductorDelegate

{ @Nullable private P presenter; @Nullable private Bundle bundle; - private boolean presenterHasView = false; private PresenterFactory

factory; @@ -22,8 +21,8 @@ public class NucleusConductorDelegate

{ if (presenter == null) { presenter = factory.createPresenter(); presenter.create(bundle); + bundle = null; } - bundle = null; return presenter; } @@ -37,31 +36,26 @@ public class NucleusConductorDelegate

{ } void onRestoreInstanceState(Bundle presenterState) { - if (presenter != null) - throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()"); bundle = presenterState; } void onTakeView(Object view) { getPresenter(); - if (presenter != null && !presenterHasView) { + if (presenter != null) { //noinspection unchecked presenter.takeView(view); - presenterHasView = true; } } void onDropView() { - if (presenter != null && presenterHasView) { + if (presenter != null) { presenter.dropView(); - presenterHasView = false; } } void onDestroy() { if (presenter != null) { presenter.destroy(); - presenter = null; } } } 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 cac5a12a9..2df28474e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -4,24 +4,20 @@ import android.content.res.Configuration import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v4.widget.DrawerLayout -import android.support.v7.app.AppCompatActivity import android.support.v7.widget.* import android.view.* -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.f2prateek.rx.preferences.Preference import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents -import com.jakewharton.rxbinding.widget.itemSelections import eu.davidea.flexibleadapter.FlexibleAdapter 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.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController @@ -43,14 +39,18 @@ import java.util.concurrent.TimeUnit /** * Controller to manage the catalogues available in the app. */ -open class CatalogueController(bundle: Bundle? = null) : +open class CatalogueController(bundle: Bundle) : NucleusController(bundle), SecondaryDrawerController, FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.EndlessScrollListener, + FlexibleAdapter.EndlessScrollListener, ChangeMangaCategoriesDialog.Listener { + constructor(source: CatalogueSource) : this(Bundle().apply { + putLong(SOURCE_ID_KEY, source.id) + }) + /** * Preferences helper. */ @@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) : */ private var adapter: FlexibleAdapter>? = null - /** - * Spinner shown in the toolbar to change the selected source. - */ - private var spinner: Spinner? = null - /** * Snackbar containing an error message when a request fails. */ @@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) : */ private var recycler: RecyclerView? = null + /** + * Drawer listener to allow swipe only for closing the drawer. + */ private var drawerListener: DrawerLayout.DrawerListener? = null - /** - * Query of the search box. - */ - private val query: String - get() = presenter.query - - /** - * Selected index of the spinner (selected source). - */ - private var selectedIndex: Int = 0 - /** * 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. + */ private var progressItem: ProgressItem? = null init { @@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) : } override fun getTitle(): String? { - return "" + return presenter.source.name } override fun createPresenter(): CataloguePresenter { - return CataloguePresenter() + return CataloguePresenter(args.getLong(SOURCE_ID_KEY)) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) : adapter = FlexibleAdapter(null, this) setupRecycler(view) - // Create toolbar spinner - val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext - ?: activity - - val spinnerAdapter = ArrayAdapter(themedContext, - android.R.layout.simple_spinner_item, presenter.sources) - spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item) - - val onItemSelected: (Int) -> Unit = { position -> - val source = spinnerAdapter.getItem(position) - if (!presenter.isValidSource(source)) { - spinner?.setSelection(selectedIndex) - activity?.toast(R.string.source_requires_login) - } else if (source != presenter.source) { - selectedIndex = position - showProgressBar() - adapter?.clear() - presenter.setActiveSource(source) - navView?.setFilters(presenter.filterItems) - activity?.invalidateOptionsMenu() - } - } - - selectedIndex = presenter.sources.indexOf(presenter.source) - - spinner = Spinner(themedContext).apply { - adapter = spinnerAdapter - setSelection(selectedIndex) - itemSelections() - .skip(1) - .filter { it != AdapterView.INVALID_POSITION } - .subscribeUntilDestroy { onItemSelected(it) } - } - - activity?.toolbar?.addView(spinner) + navView?.setFilters(presenter.filterItems) view.progress?.visible() } override fun onDestroyView(view: View) { super.onDestroyView(view) - activity?.toolbar?.removeView(spinner) numColumnsSubscription?.unsubscribe() numColumnsSubscription = null searchViewSubscription?.unsubscribe() searchViewSubscription = null adapter = null - spinner = null snack = null recycler = null } @@ -187,10 +144,7 @@ open class CatalogueController(bundle: Bundle? = null) : } navView.setFilters(presenter.filterItems) - navView.post { - if (isAttached && !drawer.isDrawerOpen(navView)) - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) - } + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) navView.onSearchClicked = { val allDefault = presenter.sourceFilters == presenter.source.getFilterList() @@ -228,6 +182,7 @@ open class CatalogueController(bundle: Bundle? = null) : val recycler = if (presenter.isListMode) { RecyclerView(view.context).apply { + id = R.id.recycler layoutManager = LinearLayoutManager(context) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) } @@ -267,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) : menu.findItem(R.id.action_search).apply { val searchView = actionView as SearchView + val query = presenter.query if (!query.isBlank()) { expandActionView() searchView.setQuery(query, true) @@ -330,9 +286,14 @@ open class CatalogueController(bundle: Bundle? = null) : */ private fun searchWithQuery(newQuery: String) { // If text didn't change, do nothing - if (query == newQuery) + if (presenter.query == newQuery) return + // FIXME dirty fix to restore the toolbar buttons after closing search mode. + if (newQuery == "") { + activity?.invalidateOptionsMenu() + } + showProgressBar() adapter?.clear() @@ -444,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) : */ fun getColumnsPreferenceForCurrentOrientation(): Preference { return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) - presenter.prefs.portraitColumns() + preferences.portraitColumns() else - presenter.prefs.landscapeColumns() + preferences.landscapeColumns() } /** @@ -555,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) : presenter.updateMangaCategories(manga, categories) } + protected companion object { + const val SOURCE_ID_KEY = "sourceId" + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt index 4cd6554fa..3fdba1e2e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt @@ -1,10 +1,10 @@ package eu.kanade.tachiyomi.ui.catalogue import android.view.View -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.widget.StateImageViewTarget import kotlinx.android.synthetic.main.catalogue_grid_item.view.* @@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA } override fun setImage(manga: Manga) { - Glide.clear(view.thumbnail) + GlideApp.with(view.context).clear(view.thumbnail) if (!manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(view.context) + GlideApp.with(view.context) .load(manga) - .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .diskCacheStrategy(DiskCacheStrategy.DATA) .centerCrop() .skipMemoryCache(true) .placeholder(android.R.color.transparent) .into(StateImageViewTarget(view.thumbnail, view.progress)) - } } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt index b6207b8a1..0a3209810 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt @@ -1,39 +1,40 @@ package eu.kanade.tachiyomi.ui.catalogue import android.view.Gravity -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import com.f2prateek.rx.preferences.Preference import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.android.synthetic.main.catalogue_grid_item.view.* -class CatalogueItem(val manga: Manga) : AbstractFlexibleItem() { +class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference) : + AbstractFlexibleItem() { override fun getLayoutRes(): Int { - return R.layout.catalogue_grid_item + return if (catalogueAsList.getOrDefault()) + R.layout.catalogue_list_item + else + R.layout.catalogue_grid_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, - inflater: LayoutInflater, - parent: ViewGroup): CatalogueHolder { - - if (parent is AutofitRecyclerView) { - val view = parent.inflate(R.layout.catalogue_grid_item).apply { + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueHolder { + val parent = adapter.recyclerView + return if (parent is AutofitRecyclerView) { + 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) } - return CatalogueGridHolder(view, adapter) + CatalogueGridHolder(view, adapter) } else { - val view = parent.inflate(R.layout.catalogue_list_item) - return CatalogueListHolder(view, adapter) + CatalogueListHolder(view, adapter) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt index 5b782b167..a12ec77d2 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt @@ -1,12 +1,11 @@ package eu.kanade.tachiyomi.ui.catalogue import android.view.View -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.util.getResourceColor -import jp.wasabeef.glide.transformations.CropCircleTransformation import kotlinx.android.synthetic.main.catalogue_list_item.view.* /** @@ -37,13 +36,13 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) : } override fun setImage(manga: Manga) { - Glide.clear(view.thumbnail) + GlideApp.with(view.context).clear(view.thumbnail) if (!manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(view.context) + GlideApp.with(view.context) .load(manga) - .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .diskCacheStrategy(DiskCacheStrategy.DATA) .centerCrop() - .bitmapTransform(CropCircleTransformation(view.context)) + .circleCrop() .dontAnimate() .skipMemoryCache(true) .placeholder(android.R.color.transparent) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt index 1bf4d3c0f..c55727129 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt @@ -34,7 +34,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: } fun setFilters(items: List>) { - adapter.updateDataSet(items.toMutableList()) + adapter.updateDataSet(items) } } \ No newline at end of file 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 69fd34db7..62a6977ba 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -9,15 +9,11 @@ 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.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager 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.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.catalogue.filter.* import rx.Observable @@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get * Presenter of [CatalogueController]. */ open class CataloguePresenter( - val sourceManager: SourceManager = Injekt.get(), - val db: DatabaseHelper = Injekt.get(), - val prefs: PreferencesHelper = Injekt.get(), - 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() { /** - * Enabled sources. + * Selected source. */ - val sources by lazy { getEnabledSources() } - - /** - * Active source. - */ - lateinit var source: CatalogueSource - private set + val source = sourceManager.get(sourceId) as CatalogueSource /** * Query from the view. @@ -106,7 +97,6 @@ open class CataloguePresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - source = getLastUsedSource() sourceFilters = source.getFilterList() if (savedState != null) { @@ -141,17 +131,19 @@ open class CataloguePresenter( val sourceId = source.id + val catalogueAsList = prefs.catalogueAsList() + // Prepare the pager. pagerSubscription?.let { remove(it) } pagerSubscription = pager.results() .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) } + .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } } .observeOn(AndroidSchedulers.mainThread()) - .subscribeReplay({ view, pair -> - view.onAddPage(pair.first, pair.second) - }, { view, error -> + .subscribeReplay({ view, (page, mangas) -> + view.onAddPage(page, mangas) + }, { _, error -> Timber.e(error) }) @@ -167,7 +159,7 @@ open class CataloguePresenter( pageSubscription?.let { remove(it) } pageSubscription = Observable.defer { pager.requestNext() } - .subscribeFirst({ view, page -> + .subscribeFirst({ _, _ -> // Nothing to do when onNext is emitted. }, CatalogueController::onAddPageError) } @@ -179,19 +171,6 @@ open class CataloguePresenter( return pager.hasNextPage } - /** - * Sets the active source and restarts the pager. - * - * @param source the new active source. - */ - fun setActiveSource(source: CatalogueSource) { - prefs.lastUsedCatalogueSource().set(source.id) - this.source = source - sourceFilters = source.getFilterList() - - restartPager(query = "", filters = FilterList()) - } - /** * Sets the display mode. * @@ -267,50 +246,6 @@ open class CataloguePresenter( .onErrorResumeNext { Observable.just(manga) } } - /** - * Returns the last used source from preferences or the first valid source. - * - * @return a source. - */ - fun getLastUsedSource(): CatalogueSource { - val id = prefs.lastUsedCatalogueSource().get() ?: -1 - val source = sourceManager.get(id) - if (!isValidSource(source) || source !in sources) { - return sources.first { isValidSource(it) } - } - return source as CatalogueSource - } - - /** - * Checks if the given source is valid. - * - * @param source the source to check. - * @return true if the source is valid, false otherwise. - */ - open fun isValidSource(source: Source?): Boolean { - if (source == null) return false - - if (source is LoginSource) { - return source.isLogged() || - (prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "") - } - return true - } - - /** - * Returns a list of enabled sources ordered by language and name. - */ - open protected fun getEnabledSources(): List { - val languages = prefs.enabledLanguages().getOrDefault() - val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() - - return sourceManager.getCatalogueSources() - .filter { it.lang in languages } - .filterNot { it.id.toString() in hiddenCatalogues } - .sortedBy { "(${it.lang}) ${it.name}" } + - sourceManager.get(LocalSource.ID) as LocalSource - } - /** * Adds or removes a manga from the library. * @@ -370,13 +305,12 @@ open class CataloguePresenter( } is Filter.Sort -> { val group = SortGroup(it) - val subItems = it.values.mapNotNull { + val subItems = it.values.map { SortItem(it, group) } group.subItems = subItems group } - else -> null } } } @@ -407,7 +341,7 @@ open class CataloguePresenter( * @param categories the selected categories. * @param manga the manga to move. */ - fun moveMangaToCategories(manga: Manga, categories: List) { + private fun moveMangaToCategories(manga: Manga, categories: List) { val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } db.setMangaCategories(mc, listOf(manga)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt index 1f1d75b72..279017a74 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt @@ -1,30 +1,27 @@ package eu.kanade.tachiyomi.ui.catalogue -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R class ProgressItem : AbstractFlexibleItem() { - var loadMore = true + private var loadMore = true override fun getLayoutRes(): Int { return R.layout.catalogue_progress_item } - override fun createViewHolder(adapter: FlexibleAdapter>, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List) { + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List) { holder.progressBar.visibility = View.GONE holder.progressMessage.visibility = View.GONE @@ -45,8 +42,8 @@ class ProgressItem : AbstractFlexibleItem() { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { - val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar - val progressMessage = view.findViewById(R.id.progress_message) as TextView + val progressBar: ProgressBar = view.findViewById(R.id.progress_bar) + val progressMessage: TextView = view.findViewById(R.id.progress_message) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt index d9bab855e..5ad10faf8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.catalogue.filter -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.CheckBox import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem @@ -16,8 +14,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -32,10 +30,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) { - val check = itemView.findViewById(R.id.nav_view_item) as CheckBox + val check: CheckBox = itemView.findViewById(R.id.nav_view_item) } } \ No newline at end of file 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 c023ce596..f5839fa98 100755 --- 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 @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.catalogue.filter -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import eu.davidea.flexibleadapter.FlexibleAdapter @@ -19,8 +17,12 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun getItemViewType(): Int { + return 101 + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -34,10 +36,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem) : AbstractExpandableHeaderItem) : ExpandableViewHolder(view, adapter, true) { - val title = itemView.findViewById(R.id.title) as TextView - val icon = itemView.findViewById(R.id.expand_icon) as ImageView + val title: TextView = itemView.findViewById(R.id.title) + val icon: ImageView = itemView.findViewById(R.id.expand_icon) override fun shouldNotifyParentOnClick(): Boolean { return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt index a76612167..fc929af2e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter import android.annotation.SuppressLint import android.support.design.R -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.TextView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractHeaderItem @@ -18,8 +16,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -29,10 +27,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem) : SelectItem(filter), ISection override fun equals(other: Any?): Boolean { if (this === other) return true - if (other is SelectSectionItem) { - return filter == other.filter - } - return false + if (javaClass != other?.javaClass) return false + return filter == (other as SelectSectionItem).filter } override fun hashCode(): Int { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt index ad840475e..6a3e9005e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.catalogue.filter -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Spinner import android.widget.TextView @@ -19,8 +17,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -32,18 +30,16 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem - filter.state = position + spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos -> + filter.state = pos } spinner.setSelection(filter.state) } override fun equals(other: Any?): Boolean { if (this === other) return true - if (other is SelectItem) { - return filter == other.filter - } - return false + if (javaClass != other?.javaClass) return false + return filter == (other as SelectItem).filter } override fun hashCode(): Int { @@ -52,7 +48,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) { - val text = itemView.findViewById(R.id.nav_view_item_text) as TextView - val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner + val text: TextView = itemView.findViewById(R.id.nav_view_item_text) + val spinner: Spinner = itemView.findViewById(R.id.nav_view_item) } } \ No newline at end of file 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 8420f2f7d..61fa80c8b 100755 --- 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 @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter import android.annotation.SuppressLint import android.support.design.R -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.viewholders.FlexibleViewHolder @@ -17,8 +15,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -27,10 +25,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem>() { - // Use an id instead of the layout res to allow to reuse the layout. override fun getLayoutRes(): Int { - return R.id.catalogue_filter_sort_group + return R.layout.navigation_view_group } - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(R.layout.navigation_view_group, parent, false), adapter) + override fun getItemViewType(): Int { + return 100 + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -32,10 +33,8 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem(group) { - // Use an id instead of the layout res to allow to reuse the layout. override fun getLayoutRes(): Int { - return R.id.catalogue_filter_sort_item + return R.layout.navigation_view_checkedtext } - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(R.layout.navigation_view_checkedtext, parent, false), adapter) + override fun getItemViewType(): Int { + return 102 + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -54,10 +55,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem override fun equals(other: Any?): Boolean { if (this === other) return true - if (other is SortItem) { - return name == other.name && group == other.group - } - return false + if (javaClass != other?.javaClass) return false + other as SortItem + return name == other.name && group == other.group } override fun hashCode(): Int { @@ -68,7 +68,7 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { - val text = itemView.findViewById(R.id.nav_view_item) as CheckedTextView + val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt index 9d4321dcb..18c57b640 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt @@ -1,9 +1,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter import android.support.design.widget.TextInputLayout -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.EditText import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem @@ -18,8 +16,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -34,10 +32,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) { - val wrapper = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout - val edit = itemView.findViewById(R.id.nav_view_item) as EditText + val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper) + val edit: EditText = itemView.findViewById(R.id.nav_view_item) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt index 0c834b337..d122251c9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt @@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter import android.support.design.R import android.support.graphics.drawable.VectorDrawableCompat -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.CheckedTextView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem @@ -20,8 +18,12 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem, inflater: LayoutInflater, parent: ViewGroup?): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun getItemViewType(): Int { + return 103 + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -51,10 +53,8 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem) : FlexibleViewHolder(view, adapter) { - val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView + val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item) init { // Align with native checkbox diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt new file mode 100644 index 000000000..0b1b822e0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.os.Bundle +import android.os.Parcelable +import android.support.v7.widget.RecyclerView +import android.util.SparseArray +import eu.davidea.flexibleadapter.FlexibleAdapter + +/** + * Adapter that holds the search cards. + * + * @param controller instance of [CatalogueSearchController]. + */ +class CatalogueSearchAdapter(val controller: CatalogueSearchController) : + FlexibleAdapter(null, controller, true) { + + /** + * Bundle where the view state of the holders is saved. + */ + private var bundle = Bundle() + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List?) { + super.onBindViewHolder(holder, position, payloads) + restoreHolderState(holder) + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + super.onViewRecycled(holder) + saveHolderState(holder, bundle) + } + + override fun onSaveInstanceState(outState: Bundle) { + val holdersBundle = Bundle() + allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) } + outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY) + } + + /** + * Saves the view state of the given holder. + * + * @param holder The holder to save. + * @param outState The bundle where the state is saved. + */ + private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) { + val key = "holder_${holder.adapterPosition}" + val holderState = SparseArray() + holder.itemView.saveHierarchyState(holderState) + outState.putSparseParcelableArray(key, holderState) + } + + /** + * Restores the view state of the given holder. + * + * @param holder The holder to restore. + */ + private fun restoreHolderState(holder: RecyclerView.ViewHolder) { + val key = "holder_${holder.adapterPosition}" + val holderState = bundle.getSparseParcelableArray(key) + if (holderState != null) { + holder.itemView.restoreHierarchyState(holderState) + bundle.remove(key) + } + } + + private companion object { + const val HOLDER_BUNDLE_KEY = "holder_bundle" + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt new file mode 100644 index 000000000..17791f3be --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.database.models.Manga + +/** + * Adapter that holds the manga items from search results. + * + * @param controller instance of [CatalogueSearchController]. + */ +class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : + FlexibleAdapter(null, controller, true) { + + /** + * Listen for browse item clicks. + */ + val mangaClickListener: OnMangaClickListener = controller + + /** + * Listener which should be called when user clicks browse. + * Note: Should only be handled by [CatalogueSearchController] + */ + interface OnMangaClickListener { + fun onMangaClick(manga: Manga) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt new file mode 100644 index 000000000..e35a2ceb7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.View +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.widget.StateImageViewTarget +import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.* + +class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) + : FlexibleViewHolder(view, adapter) { + + init { + // Call onMangaClickListener when item is pressed. + itemView.setOnClickListener { + val item = adapter.getItem(adapterPosition) + if (item != null) { + adapter.mangaClickListener.onMangaClick(item.manga) + } + } + } + + fun bind(manga: Manga) { + itemView.tvTitle.text = manga.title + + setImage(manga) + } + + fun setImage(manga: Manga) { + GlideApp.with(itemView.context).clear(itemView.itemImage) + if (!manga.thumbnail_url.isNullOrEmpty()) { + GlideApp.with(itemView.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .centerCrop() + .skipMemoryCache(true) + .placeholder(android.R.color.transparent) + .into(StateImageViewTarget(itemView.itemImage, itemView.progress)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt new file mode 100644 index 000000000..3d43b12e9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga + +class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.catalogue_global_search_controller_card_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder { + return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder, + position: Int, payloads: List?) { + holder.bind(manga) + } + + override fun equals(other: Any?): Boolean { + if (other is CatalogueSearchCardItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id?.toInt() ?: 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt new file mode 100644 index 000000000..dcbb42f4c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt @@ -0,0 +1,184 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.SearchView +import android.view.* +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.* + +/** + * This controller shows and manages the different search result in global search. + * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter] + * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search + */ +class CatalogueSearchController(private val initialQuery: String? = null) : + NucleusController(), + CatalogueSearchCardAdapter.OnMangaClickListener { + + /** + * Adapter containing search results grouped by lang. + */ + private var adapter: CatalogueSearchAdapter? = null + + /** + * Called when controller is initialized. + */ + init { + setHasOptionsMenu(true) + } + + /** + * Initiate the view with [R.layout.catalogue_global_search_controller]. + * + * @param inflater used to load the layout xml. + * @param container containing parent views. + * @return inflated view + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View { + return inflater.inflate(R.layout.catalogue_global_search_controller, container, false) + } + + /** + * Set the title of controller. + * + * @return title. + */ + override fun getTitle(): String? { + return presenter.query + } + + /** + * Create the [CatalogueSearchPresenter] used in controller. + * + * @return instance of [CatalogueSearchPresenter] + */ + override fun createPresenter(): CatalogueSearchPresenter { + return CatalogueSearchPresenter(initialQuery) + } + + /** + * Called when manga in global search is clicked, opens manga. + * + * @param manga clicked item containing manga information. + */ + override fun onMangaClick(manga: Manga) { + // Open MangaController. + router.pushController(RouterTransaction.with(MangaController(manga, true)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + + /** + * Adds items to the options menu. + * + * @param menu menu containing options. + * @param inflater used to load the menu xml. + */ + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + // Inflate menu. + inflater.inflate(R.menu.catalogue_new_list, menu) + + // Initialize search menu + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + + searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + searchView.onActionViewExpanded() // Required to show the query in the view + searchView.setQuery(presenter.query, false) + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + return true + } + }) + + searchView.queryTextChangeEvents() + .filter { it.isSubmitted } + .subscribeUntilDestroy { + presenter.search(it.queryText().toString()) + searchItem.collapseActionView() + setTitle() // Update toolbar title + } + } + + /** + * Called when the view is created + * + * @param view view of controller + * @param savedViewState information from previous state. + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = CatalogueSearchAdapter(this) + + with(view) { + // Create recycler and set adapter. + recycler.layoutManager = LinearLayoutManager(context) + recycler.adapter = adapter + } + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun onSaveViewState(view: View, outState: Bundle) { + super.onSaveViewState(view, outState) + adapter?.onSaveInstanceState(outState) + } + + override fun onRestoreViewState(view: View, savedViewState: Bundle) { + super.onRestoreViewState(view, savedViewState) + adapter?.onRestoreInstanceState(savedViewState) + } + + /** + * Returns the view holder for the given manga. + * + * @param source used to find holder containing source + * @return the holder of the manga or null if it's not bound. + */ + private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? { + val adapter = adapter ?: return null + + adapter.allBoundViewHolders.forEach { holder -> + val item = adapter.getItem(holder.adapterPosition) + if (item != null && source.id == item.source.id) { + return holder as CatalogueSearchHolder + } + } + + return null + } + + /** + * Add search result to adapter. + * + * @param searchResult result of search. + */ + fun setItems(searchResult: List) { + adapter?.updateDataSet(searchResult) + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the initialized manga. + */ + fun onMangaInitialized(source: CatalogueSource, manga: Manga) { + getHolder(source)?.setImage(manga) + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..0714c4342 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt @@ -0,0 +1,100 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.support.v7.widget.LinearLayoutManager +import android.view.View +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.setVectorCompat +import eu.kanade.tachiyomi.util.visible +import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.* + +/** + * Holder that binds the [CatalogueSearchItem] containing catalogue cards. + * + * @param view view of [CatalogueSearchItem] + * @param adapter instance of [CatalogueSearchAdapter] + */ +class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) { + + /** + * Adapter containing manga from search results. + */ + private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller) + + private var lastBoundResults: List? = null + + init { + with(itemView) { + // Set layout horizontal. + recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + recycler.adapter = mangaAdapter + + nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp, + context.getResourceColor(android.R.attr.textColorHint)) + } + } + + /** + * Show the loading of source search result. + * + * @param item item of card. + */ + fun bind(item: CatalogueSearchItem) { + val source = item.source + val results = item.results + + with(itemView) { + // Set Title witch country code if available. + title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name + + when { + results == null -> { + progress.visible() + nothing_found.gone() + } + results.isEmpty() -> { + progress.gone() + nothing_found.visible() + } + else -> { + progress.gone() + nothing_found.gone() + } + } + if (results !== lastBoundResults) { + mangaAdapter.updateDataSet(results) + lastBoundResults = results + } + } + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the initialized manga. + */ + fun setImage(manga: Manga) { + getHolder(manga)?.setImage(manga) + } + + /** + * Returns the view holder for the given manga. + * + * @param manga the manga to find. + * @return the holder of the manga or null if it's not bound. + */ + private fun getHolder(manga: Manga): CatalogueSearchCardHolder? { + mangaAdapter.allBoundViewHolders.forEach { holder -> + val item = mangaAdapter.getItem(holder.adapterPosition) + if (item != null && item.manga.id!! == manga.id!!) { + return holder as CatalogueSearchCardHolder + } + } + + return null + } + +} 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 new file mode 100644 index 000000000..7722a3202 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt @@ -0,0 +1,64 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource + +/** + * Item that contains search result information. + * + * @param source contains information about search result. + */ +class CatalogueSearchItem(val source: CatalogueSource, val results: List?) + : AbstractFlexibleItem() { + + /** + * Set view. + * + * @return id of view + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_global_search_controller_card + } + + /** + * Create view holder (see [CatalogueSearchAdapter]. + * + * @return holder of view. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchHolder { + return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter) + } + + /** + * Bind item to view. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder, + position: Int, payloads: List?) { + holder.bind(this) + } + + /** + * Used to check if two items are equal. + * + * @return items are equal? + */ + override fun equals(other: Any?): Boolean { + if (other is CatalogueSearchItem) { + return source.id == other.source.id + } + return false + } + + /** + * Return hash code of item. + * + * @return hashcode + */ + override fun hashCode(): Int { + return source.id.toInt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt new file mode 100644 index 000000000..ebd5327d6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt @@ -0,0 +1,215 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.os.Bundle +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.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.LoginSource +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import rx.subjects.PublishSubject +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of [CatalogueSearchController] + * Function calls should be done from here. UI calls should be done from the controller. + * + * @param sourceManager manages the different sources. + * @param db manages the database calls. + * @param preferencesHelper manages the preference calls. + */ +class CatalogueSearchPresenter( + val initialQuery: String? = "", + val sourceManager: SourceManager = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), + val preferencesHelper: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + /** + * Enabled sources. + */ + val sources by lazy { getEnabledSources() } + + /** + * Query from the view. + */ + var query = "" + private set + + /** + * Fetches the different sources by user settings. + */ + private var fetchSourcesSubscription: Subscription? = null + + /** + * Subject which fetches image of given manga. + */ + private val fetchImageSubject = PublishSubject.create, Source>>() + + /** + * Subscription for fetching images of manga. + */ + private var fetchImageSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Perform a search with previous or initial state + search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty()) + } + + override fun onDestroy() { + fetchSourcesSubscription?.unsubscribe() + fetchImageSubscription?.unsubscribe() + super.onDestroy() + } + + override fun onSave(state: Bundle) { + state.putString(CataloguePresenter::query.name, query) + super.onSave(state) + } + + /** + * Returns a list of enabled sources ordered by language and name. + * + * @return list containing enabled sources. + */ + private fun getEnabledSources(): List { + 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}" } + } + + /** + * Initiates a search for mnaga per catalogue. + * + * @param query query on which to search. + */ + fun search(query: String) { + // Return if there's nothing to do + if (this.query == query) return + + // Update query + this.query = query + + // Create image fetch subscription + initializeFetchImageSubscription() + + // Create items with the initial state + val initialItems = sources.map { CatalogueSearchItem(it, null) } + var items = initialItems + + fetchSourcesSubscription?.unsubscribe() + fetchSourcesSubscription = Observable.from(sources) + .flatMap({ source -> + source.fetchSearchManga(1, query, FilterList()) + .subscribeOn(Schedulers.io()) + .onExceptionResumeNext(Observable.empty()) // Ignore timeouts. + .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 { CatalogueSearchItem(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) + }) + } + + /** + * Initialize a list of manga. + * + * @param manga the list of manga to initialize. + */ + private fun fetchImage(manga: List, source: Source) { + fetchImageSubject.onNext(Pair(manga, source)) + } + + /** + * Subscribes to the initializer of manga details and updates the view if needed. + */ + 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) + }) + } + + /** + * Returns an observable of manga that initializes the given manga. + * + * @param manga the manga to initialize. + * @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) } + } + + /** + * Returns a manga from the database for the given manga from network. It creates a new entry + * if the manga is not yet in the database. + * + * @param sManga the manga from the source. + * @return a manga from the database. + */ + private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { + var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking() + if (localManga == null) { + val newManga = Manga.create(sManga.url, sManga.title, sourceId) + newManga.copyFrom(sManga) + val result = db.insertManga(newManga).executeAsBlocking() + newManga.id = result.insertedId() + localManga = newManga + } + return localManga + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt new file mode 100644 index 000000000..d2e15169c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor + +/** + * Adapter that holds the catalogue cards. + * + * @param controller instance of [CatalogueMainController]. + */ +class CatalogueMainAdapter(val controller: CatalogueMainController) : + FlexibleAdapter>(null, controller, true) { + + val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) + + init { + setDisplayHeadersAtStartUp(true) + } + + /** + * Listener for browse item clicks. + */ + val browseClickListener: OnBrowseClickListener = controller + + /** + * Listener for latest item clicks. + */ + val latestClickListener: OnLatestClickListener = controller + + /** + * Listener which should be called when user clicks browse. + * Note: Should only be handled by [CatalogueMainController] + */ + interface OnBrowseClickListener { + fun onBrowseClick(position: Int) + } + + /** + * Listener which should be called when user clicks latest. + * Note: Should only be handled by [CatalogueMainController] + */ + interface OnLatestClickListener { + fun onLatestClick(position: Int) + } +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt new file mode 100644 index 000000000..76ea5e513 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt @@ -0,0 +1,238 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.SearchView +import android.view.* +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 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.source.CatalogueSource +import eu.kanade.tachiyomi.source.online.LoginSource +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController +import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController +import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController +import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog +import kotlinx.android.synthetic.main.catalogue_main_controller.view.* +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * This controller shows and manages the different catalogues enabled by the user. + * This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter] + * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues. + * [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click. + * [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click + */ +class CatalogueMainController : NucleusController(), + SourceLoginDialog.Listener, + FlexibleAdapter.OnItemClickListener, + CatalogueMainAdapter.OnBrowseClickListener, + CatalogueMainAdapter.OnLatestClickListener { + + /** + * Application preferences. + */ + private val preferences: PreferencesHelper = Injekt.get() + + /** + * Adapter containing sources. + */ + private var adapter : CatalogueMainAdapter? = null + + /** + * Called when controller is initialized. + */ + init { + // Enable the option menu + setHasOptionsMenu(true) + } + + /** + * Set the title of controller. + * + * @return title. + */ + override fun getTitle(): String? { + return applicationContext?.getString(R.string.label_catalogues) + } + + /** + * Create the [CatalogueMainPresenter] used in controller. + * + * @return instance of [CatalogueMainPresenter] + */ + override fun createPresenter(): CatalogueMainPresenter { + return CatalogueMainPresenter() + } + + /** + * Initiate the view with [R.layout.catalogue_main_controller]. + * + * @param inflater used to load the layout xml. + * @param container containing parent views. + * @return inflated view. + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.catalogue_main_controller, container, false) + } + + /** + * Called when the view is created + * + * @param view view of controller + * @param savedViewState information from previous state. + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = CatalogueMainAdapter(this) + + with(view) { + // Create recycler and set adapter. + recycler.layoutManager = LinearLayoutManager(context) + recycler.adapter = adapter + recycler.addItemDecoration(SourceDividerItemDecoration(context)) + } + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) { + presenter.updateSources() + } + } + + /** + * Called when login dialog is closed, refreshes the adapter. + * + * @param source clicked item containing source information. + */ + override fun loginDialogClosed(source: LoginSource) { + if (source.isLogged()) { + adapter?.clear() + presenter.loadSources() + } + } + + /** + * Called when item is clicked + */ + override fun onItemClick(position: Int): Boolean { + val item = adapter?.getItem(position) as? SourceItem ?: return false + val source = item.source + if (source is LoginSource && !source.isLogged()) { + val dialog = SourceLoginDialog(source) + dialog.targetController = this + dialog.showDialog(router) + } else { + // Open the catalogue view. + openCatalogue(source, CatalogueController(source)) + } + return false + } + + /** + * Called when browse is clicked in [CatalogueMainAdapter] + */ + override fun onBrowseClick(position: Int) { + onItemClick(position) + } + + /** + * Called when latest is clicked in [CatalogueMainAdapter] + */ + override fun onLatestClick(position: Int) { + val item = adapter?.getItem(position) as? SourceItem ?: return + openCatalogue(item.source, LatestUpdatesController(item.source)) + } + + /** + * Opens a catalogue with the given controller. + */ + private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) { + preferences.lastUsedCatalogueSource().set(source.id) + router.pushController(RouterTransaction.with(controller) + .popChangeHandler(FadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler())) + } + + /** + * Adds items to the options menu. + * + * @param menu menu containing options. + * @param inflater used to load the menu xml. + */ + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + // 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 + + // Change hint to show global search. + searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint) + + // Create query listener which opens the global search view. + searchView.queryTextChangeEvents() + .filter { it.isSubmitted } + .subscribeUntilDestroy { + val query = it.queryText().toString() + router.pushController((RouterTransaction.with(CatalogueSearchController(query))) + .popChangeHandler(FadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler())) + } + } + + /** + * Called when an option menu item has been selected by the user. + * + * @param item The selected item. + * @return True if this event has been consumed, false if it has not. + */ + 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())) + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Called to update adapter containing sources. + */ + fun setSources(sources: List>) { + adapter?.updateDataSet(sources) + } + + /** + * Called to set the last used catalogue at the top of the view. + */ + fun setLastUsedSource(item: SourceItem?) { + adapter?.removeAllScrollableHeaders() + if (item != null) { + adapter?.addScrollableHeader(item) + } + } + + class SettingsSourcesFadeChangeHandler : FadeChangeHandler() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt new file mode 100644 index 000000000..a0745a26a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt @@ -0,0 +1,104 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.os.Bundle +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.LocalSource +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +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.* +import java.util.concurrent.TimeUnit + +/** + * Presenter of [CatalogueMainController] + * Function calls should be done from here. UI calls should be done from the controller. + * + * @param sourceManager manages the different sources. + * @param preferences application preferences. + */ +class CatalogueMainPresenter( + val sourceManager: SourceManager = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + /** + * Enabled sources. + */ + var sources = getEnabledSources() + + /** + * Subscription for retrieving enabled sources. + */ + private var sourceSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Load enabled and last used sources + loadSources() + loadLastUsedSource() + } + + /** + * Unsubscribe and create a new subscription to fetch enabled sources. + */ + fun loadSources() { + sourceSubscription?.unsubscribe() + + val map = TreeMap> { d1, d2 -> + // Catalogues without a lang defined will be placed at the end + when { + d1 == "" && d2 != "" -> 1 + d2 == "" && d1 != "" -> -1 + else -> d1.compareTo(d2) + } + } + val byLang = sources.groupByTo(map, { it.lang }) + val sourceItems = byLang.flatMap { + val langItem = LangItem(it.key) + it.value.map { source -> SourceItem(source, langItem) } + } + + sourceSubscription = Observable.just(sourceItems) + .subscribeLatestCache(CatalogueMainController::setSources) + } + + private fun loadLastUsedSource() { + val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share() + + // Emit the first item immediately but delay subsequent emissions by 500ms. + Observable.merge( + sharedObs.take(1), + sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())) + .distinctUntilChanged() + .map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } } + .subscribeLatestCache(CatalogueMainController::setLastUsedSource) + } + + fun updateSources() { + sources = getEnabledSources() + loadSources() + } + + /** + * Returns a list of enabled sources ordered by language and name. + * + * @return list containing enabled sources. + */ + private fun getEnabledSources(): List { + val languages = preferences.enabledLanguages().getOrDefault() + val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault() + + return sourceManager.getCatalogueSources() + .filter { it.lang in languages } + .filterNot { it.id.toString() in hiddenCatalogues } + .sortedBy { "(${it.lang}) ${it.name}" } + + sourceManager.get(LocalSource.ID) as LocalSource + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt new file mode 100644 index 000000000..02dcea146 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.* +import java.util.* + +class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { + + fun bind(item: LangItem) { + itemView.title.text = when { + item.code == "" -> itemView.context.getString(R.string.other_source) + else -> { + val locale = Locale(item.code) + locale.getDisplayName(locale).capitalize() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt new file mode 100644 index 000000000..815ad7495 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.kanade.tachiyomi.R + +/** + * Item that contains the language header. + * + * @param code The lang code. + */ +data class LangItem(val code: String) : AbstractHeaderItem() { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LangHolder { + return LangHolder(view, adapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder, + position: Int, payloads: List?) { + + holder.bind(this) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt new file mode 100644 index 000000000..bb90f5307 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.support.v7.widget.RecyclerView +import android.view.View + +class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { + + private val divider: Drawable + + init { + val a = context.obtainStyledAttributes(ATTRS) + divider = a.getDrawable(0) + a.recycle() + } + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val left = parent.paddingLeft + SourceHolder.margin + val right = parent.width - parent.paddingRight - SourceHolder.margin + + val childCount = parent.childCount + for (i in 0 until childCount - 1) { + val child = parent.getChildAt(i) + if (parent.getChildViewHolder(child) is SourceHolder && + parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) { + val params = child.layoutParams as RecyclerView.LayoutParams + val top = child.bottom + params.bottomMargin + val bottom = top + divider.intrinsicHeight + + divider.setBounds(left, top, right, bottom) + divider.draw(c) + } + } + } + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State) { + outRect.set(0, 0, 0, divider.intrinsicHeight) + } + + companion object { + private val ATTRS = intArrayOf(android.R.attr.listDivider) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt new file mode 100644 index 000000000..ddc8914b0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt @@ -0,0 +1,107 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.os.Build +import android.view.View +import android.view.ViewGroup +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.online.LoginSource +import eu.kanade.tachiyomi.util.dpToPx +import eu.kanade.tachiyomi.util.getRound +import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.visible +import io.github.mthli.slice.Slice +import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.* + +class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) { + + private val slice = Slice(itemView.card).apply { + setColor(adapter.cardBackground) + } + + init { + itemView.source_browse.setOnClickListener { + adapter.browseClickListener.onBrowseClick(adapterPosition) + } + + itemView.source_latest.setOnClickListener { + adapter.latestClickListener.onLatestClick(adapterPosition) + } + } + + fun bind(item: SourceItem) { + val source = item.source + with(itemView) { + setCardEdges(item) + + // Set source name + title.text = source.name + + // Set circle letter image. + post { + image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + } + + // If source is login, show only login option + if (source is LoginSource && !source.isLogged()) { + source_browse.setText(R.string.login) + source_latest.gone() + } else { + source_browse.setText(R.string.browse) + source_latest.visible() + } + } + } + + private fun setCardEdges(item: SourceItem) { + // Position of this item in its header. Defaults to 0 when header is null. + var position = 0 + + // Number of items in the header of this item. Defaults to 1 when header is null. + var count = 1 + + if (item.header != null) { + val sectionItems = mAdapter.getSectionItems(item.header) + position = sectionItems.indexOf(item) + count = sectionItems.size + } + + when { + // Only one item in the card + count == 1 -> applySlice(2f, false, false, true, true) + // First item of the card + position == 0 -> applySlice(2f, false, true, true, false) + // Last item of the card + position == count - 1 -> applySlice(2f, true, false, false, true) + // Middle item + else -> applySlice(0f, false, false, false, false) + } + } + + private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, + topShadow: Boolean, bottomShadow: Boolean) { + + slice.setRadius(radius) + slice.showLeftTopRect(topRect) + slice.showRightTopRect(topRect) + slice.showLeftBottomRect(bottomRect) + slice.showRightBottomRect(bottomRect) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + slice.showTopEdgeShadow(topShadow) + slice.showBottomEdgeShadow(bottomShadow) + } + setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) + } + + private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { + val v = itemView.card + if (v.layoutParams is ViewGroup.MarginLayoutParams) { + val p = v.layoutParams as ViewGroup.MarginLayoutParams + p.setMargins(left, top, right, bottom) + } + } + + companion object { + val margin = 8.dpToPx + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt new file mode 100644 index 000000000..5031b81d9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractSectionableItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource + +/** + * Item that contains source information. + * + * @param source Instance of [CatalogueSource] containing source information. + * @param header The header for this item. + */ +data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) : + AbstractSectionableItem(header) { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card_item + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { + return SourceHolder(view, adapter as CatalogueMainAdapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder, + position: Int, payloads: List?) { + + holder.bind(this) + } + +} \ 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 907a5a04a..0ad0eb90c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt @@ -20,14 +20,14 @@ class CategoryAdapter(controller: CategoryController) : */ override fun clearSelection() { super.clearSelection() - (0 until itemCount).forEach { getItem(it).isSelected = false } + (0 until itemCount).forEach { getItem(it)?.isSelected = false } } /** * Clears the active selections from the model. */ fun clearModelSelection() { - selectedPositions.forEach { getItem(it).isSelected = false } + selectedPositions.forEach { getItem(it)?.isSelected = false } } /** @@ -37,7 +37,7 @@ class CategoryAdapter(controller: CategoryController) : */ override fun toggleSelection(position: Int) { super.toggleSelection(position) - getItem(position).isSelected = isSelected(position) + getItem(position)?.isSelected = isSelected(position) } interface OnItemReleaseListener { 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 81243c50d..87df04333 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,6 +1,7 @@ package eu.kanade.tachiyomi.ui.category import android.os.Bundle +import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity import android.support.v7.view.ActionMode import android.support.v7.widget.LinearLayoutManager @@ -8,11 +9,12 @@ import android.support.v7.widget.RecyclerView import android.view.* import com.jakewharton.rxbinding.view.clicks 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.util.toast -import eu.kanade.tachiyomi.widget.UndoHelper import kotlinx.android.synthetic.main.categories_controller.view.* /** @@ -38,7 +40,7 @@ class CategoryController : NucleusController(), private var adapter: CategoryAdapter? = null /** - * Undo helper for deleting categories. + * Undo helper used for restoring a deleted category. */ private var undoHelper: UndoHelper? = null @@ -79,6 +81,7 @@ class CategoryController : NucleusController(), recycler.setHasFixedSize(true) recycler.adapter = adapter adapter?.isHandleDragEnabled = true + adapter?.isPermanentDelete = false fab.clicks().subscribeUntilDestroy { CategoryCreateDialog(this@CategoryController).showDialog(router, null) @@ -93,7 +96,8 @@ class CategoryController : NucleusController(), */ override fun onDestroyView(view: View) { super.onDestroyView(view) - undoHelper?.dismissNow() // confirm categories deletion if required + // Manually call callback to delete categories if required + undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) undoHelper = null actionMode = null adapter = null @@ -106,7 +110,7 @@ class CategoryController : NucleusController(), */ fun setCategories(categories: List) { actionMode?.finish() - adapter?.updateDataSet(categories.toMutableList()) + adapter?.updateDataSet(categories) val selected = categories.filter { it.isSelected } if (selected.isNotEmpty()) { selected.forEach { onItemLongClick(categories.indexOf(it)) } @@ -126,7 +130,7 @@ class CategoryController : NucleusController(), // Inflate menu. mode.menuInflater.inflate(R.menu.category_selection, menu) // Enable adapter multi selection. - adapter?.mode = FlexibleAdapter.MODE_MULTI + adapter?.mode = SelectableAdapter.Mode.MULTI return true } @@ -161,26 +165,20 @@ class CategoryController : NucleusController(), when (item.itemId) { R.id.action_delete -> { - undoHelper = UndoHelper(adapter, this).apply { - withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener { - override fun onPreAction(): Boolean { - adapter.clearModelSelection() - return false - } + undoHelper = UndoHelper(adapter, this) + undoHelper?.start(adapter.selectedPositions, view!!, + R.string.snack_categories_deleted, R.string.action_undo, 3000) - override fun onPostAction() { - mode.finish() - } - }) - remove(adapter.selectedPositions, view!!, - R.string.snack_categories_deleted, R.string.action_undo, 3000) - } + mode.finish() } R.id.action_edit -> { // Edit selected category if (adapter.selectedItemCount == 1) { val position = adapter.selectedPositions.first() - editCategory(adapter.getItem(position).category) + val category = adapter.getItem(position)?.category + if (category != null) { + editCategory(category) + } } } else -> return false @@ -195,7 +193,7 @@ class CategoryController : NucleusController(), */ override fun onDestroyActionMode(mode: ActionMode) { // Reset adapter to single selection - adapter?.mode = FlexibleAdapter.MODE_IDLE + adapter?.mode = SelectableAdapter.Mode.IDLE adapter?.clearSelection() actionMode = null } @@ -260,7 +258,7 @@ class CategoryController : NucleusController(), */ override fun onItemReleased(position: Int) { val adapter = adapter ?: return - val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category } + val categories = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.category } presenter.reorderCategories(categories) } @@ -269,18 +267,21 @@ class CategoryController : NucleusController(), * * @param action The action performed. */ - override fun onUndoConfirmed(action: Int) { + override fun onActionCanceled(action: Int) { adapter?.restoreDeletedItems() + undoHelper = 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 onDeleteConfirmed(action: Int) { + override fun onActionConfirmed(action: Int, event: Int) { val adapter = adapter ?: return presenter.deleteCategories(adapter.deletedItems.map { it.category }) + undoHelper = null } /** 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 906bbb910..0dd3c1fa7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt @@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.util.getRound import kotlinx.android.synthetic.main.categories_item.view.* /** @@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol // Update circle letter image. itemView.post { - itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase())) + itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false)) } } - /** - * Returns circle letter image. - * - * @param text The first letter of string. - */ - private fun getRound(text: String): TextDrawable { - val size = Math.min(itemView.image.width, itemView.image.height) - return TextDrawable.builder() - .beginConfig() - .width(size) - .height(size) - .textColor(Color.WHITE) - .useFont(Typeface.DEFAULT) - .endConfig() - .buildRound(text, ColorGenerator.MATERIAL.getColor(text)) - } - /** * Called when an item is released. * 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 40c52cd16..7f24ab528 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt @@ -1,12 +1,10 @@ package eu.kanade.tachiyomi.ui.category -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.util.inflate /** * Category item for a recycler view. @@ -28,15 +26,11 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem, - inflater: LayoutInflater, - parent: ViewGroup): CategoryHolder { - - return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CategoryHolder { + return CategoryHolder(view, adapter as CategoryAdapter) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt index 4e5b5e288..0577176e9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt @@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter() { downloadQueue.getUpdatedObservable() .observeOn(AndroidSchedulers.mainThread()) .map { ArrayList(it) } - .subscribeLatestCache(DownloadController::onNextDownloads, { view, error -> + .subscribeLatestCache(DownloadController::onNextDownloads, { _, error -> Timber.e(error) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt index 730b5e991..072980607 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt @@ -1,19 +1,25 @@ package eu.kanade.tachiyomi.ui.latest_updates +import android.os.Bundle import android.support.v4.widget.DrawerLayout import android.view.Menu import android.view.ViewGroup import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter /** - * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. + * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController]. */ -class LatestUpdatesController : CatalogueController() { +class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) { + + constructor(source: CatalogueSource) : this(Bundle().apply { + putLong(SOURCE_ID_KEY, source.id) + }) override fun createPresenter(): CataloguePresenter { - return LatestUpdatesPresenter() + return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) } override fun onPrepareOptionsMenu(menu: Menu) { @@ -30,4 +36,4 @@ class LatestUpdatesController : CatalogueController() { } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt index 924425b62..2e0ea07fe 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt @@ -1,7 +1,5 @@ package eu.kanade.tachiyomi.ui.latest_updates -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.Pager @@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager /** * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter. */ -class LatestUpdatesPresenter : CataloguePresenter() { +class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) { override fun createPager(query: String, filters: FilterList): Pager { return LatestUpdatesPager(source) } - override fun getEnabledSources(): List { - return super.getEnabledSources().filter { it.supportsLatest } - } - - override fun isValidSource(source: Source?): Boolean { - return super.isValidSource(source) && (source as CatalogueSource).supportsLatest - } - } \ No newline at end of file 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 index cb19c14f9..d99f61198 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -6,6 +6,7 @@ import android.support.v7.widget.RecyclerView import android.util.AttributeSet import android.widget.FrameLayout import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga @@ -103,9 +104,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att this.category = category adapter.mode = if (controller.selectedMangas.isNotEmpty()) { - FlexibleAdapter.MODE_MULTI + SelectableAdapter.Mode.MULTI } else { - FlexibleAdapter.MODE_SINGLE + SelectableAdapter.Mode.SINGLE } subscriptions += controller.searchRelay @@ -126,7 +127,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att subscriptions.clear() } - override fun onDetachedFromWindow() { subscriptions.clear() super.onDetachedFromWindow() @@ -145,7 +145,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att // Update the category with its manga. adapter.setItems(mangaForCategory) - if (adapter.mode == FlexibleAdapter.MODE_MULTI) { + if (adapter.mode == SelectableAdapter.Mode.MULTI) { controller.selectedMangas.forEach { manga -> val position = adapter.indexOf(manga) if (position != -1 && !adapter.isSelected(position)) { @@ -165,19 +165,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att private fun onSelectionChanged(event: LibrarySelectionEvent) { when (event) { is LibrarySelectionEvent.Selected -> { - if (adapter.mode != FlexibleAdapter.MODE_MULTI) { - adapter.mode = FlexibleAdapter.MODE_MULTI + if (adapter.mode != SelectableAdapter.Mode.MULTI) { + adapter.mode = SelectableAdapter.Mode.MULTI } findAndToggleSelection(event.manga) } is LibrarySelectionEvent.Unselected -> { findAndToggleSelection(event.manga) if (controller.selectedMangas.isEmpty()) { - adapter.mode = FlexibleAdapter.MODE_SINGLE + adapter.mode = SelectableAdapter.Mode.SINGLE } } is LibrarySelectionEvent.Cleared -> { - adapter.mode = FlexibleAdapter.MODE_SINGLE + adapter.mode = SelectableAdapter.Mode.SINGLE adapter.clearSelection() } } @@ -205,7 +205,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att override fun onItemClick(position: Int): Boolean { // If the action mode is created and the position is valid, toggle the selection. val item = adapter.getItem(position) ?: return false - if (adapter.mode == FlexibleAdapter.MODE_MULTI) { + if (adapter.mode == SelectableAdapter.Mode.MULTI) { toggleSelection(position) return true } else { @@ -244,4 +244,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att controller.setSelection(item.manga, !adapter.isSelected(position)) 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 4eb320353..f775f249b 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 @@ -42,6 +42,7 @@ import io.realm.Realm import io.realm.RealmResults import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.library_controller.view.* +import kotlinx.android.synthetic.main.main_activity.* import rx.Subscription import rx.android.schedulers.AndroidSchedulers import timber.log.Timber @@ -219,17 +220,14 @@ class LibraryController( drawer.addDrawerListener(it) } navView = view - - navView?.post { - if (isAttached && drawer.isDrawerOpen(navView)) - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) - } + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) navView?.onGroupClicked = { group -> when (group) { is LibraryNavigationView.FilterGroup -> onFilterChanged() is LibraryNavigationView.SortGroup -> onSortChanged() is LibraryNavigationView.DisplayGroup -> reattachAdapter() + is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() } } @@ -316,7 +314,11 @@ class LibraryController( */ private fun onFilterChanged() { presenter.requestFilterUpdate() - (activity as? AppCompatActivity)?.supportInvalidateOptionsMenu() + activity?.invalidateOptionsMenu() + } + + private fun onDownloadBadgeChanged(){ + presenter.requestDownloadBadgesUpdate() } /** 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 fcf3789e4..429e80e98 100755 --- 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,10 +1,10 @@ package eu.kanade.tachiyomi.ui.library import android.view.View -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.source.LocalSource import kotlinx.android.synthetic.main.catalogue_grid_item.view.* /** @@ -19,29 +19,39 @@ import kotlinx.android.synthetic.main.catalogue_grid_item.view.* class LibraryGridHolder( private val view: View, private val adapter: FlexibleAdapter<*> + ) : LibraryHolder(view, adapter) { /** * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. * - * @param manga the manga to bind. + * @param item the manga item to bind. */ - override fun onSetValues(manga: Manga) { + override fun onSetValues(item: LibraryItem) { // Update the title of the manga. - view.title.text = manga.title + view.title.text = item.manga.title // Update the unread count and its visibility. with(view.unread_text) { - visibility = if (manga.unread > 0) View.VISIBLE else View.GONE - text = manga.unread.toString() + visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE + text = item.manga.unread.toString() + } + // Update the download count and its visibility. + with(view.download_text) { + visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE + text = item.downloadCount.toString() + } + //set local visibility if its local manga + with(view.local_text) { + visibility = if(item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE } // Update the cover. - Glide.clear(view.thumbnail) - Glide.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) + GlideApp.with(view.context).clear(view.thumbnail) + GlideApp.with(view.context) + .load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() .into(view.thumbnail) } 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 2359377da..ff29155c4 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library import android.view.View import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.data.database.models.Manga /** * Generic class used to hold the displayed data of a manga in the library. @@ -21,8 +20,8 @@ abstract class LibraryHolder( * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. * - * @param manga the manga to bind. + * @param item the manga item to bind. */ - abstract fun onSetValues(manga: Manga) + abstract fun onSetValues(item: LibraryItem) } 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 e454dc8b2..0bd2823ef 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 @@ -1,34 +1,38 @@ package eu.kanade.tachiyomi.ui.library import android.view.Gravity -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import com.f2prateek.rx.preferences.Preference import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFilterable import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.android.synthetic.main.catalogue_grid_item.view.* -class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFilterable { - // Temp metadata holder EH +class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference) : + AbstractFlexibleItem(), IFilterable { + // Temp metadata holder (EXH) @Volatile var hasMetadata: Boolean? = null + var downloadCount = -1 + override fun getLayoutRes(): Int { - return R.layout.catalogue_grid_item + return if (libraryAsList.getOrDefault()) + R.layout.catalogue_list_item + else + R.layout.catalogue_grid_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, - inflater: LayoutInflater, - parent: ViewGroup): LibraryHolder { - + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LibraryHolder { + val parent = adapter.recyclerView return if (parent is AutofitRecyclerView) { - val view = parent.inflate(R.layout.catalogue_grid_item).apply { + view.apply { val coverHeight = parent.itemWidth / 3 * 4 card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) gradient.layoutParams = FrameLayout.LayoutParams( @@ -36,7 +40,6 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFi } LibraryGridHolder(view, adapter) } else { - val view = parent.inflate(R.layout.catalogue_list_item) LibraryListHolder(view, adapter) } } @@ -46,7 +49,7 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFi position: Int, payloads: List?) { - holder.onSetValues(manga) + holder.onSetValues(this) } /** 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 6de899532..37a23fe37 100755 --- 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,11 +1,10 @@ package eu.kanade.tachiyomi.ui.library import android.view.View -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.data.database.models.Manga -import jp.wasabeef.glide.transformations.CropCircleTransformation +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.source.LocalSource import kotlinx.android.synthetic.main.catalogue_list_item.view.* /** @@ -27,16 +26,25 @@ class LibraryListHolder( * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * holder with the given manga. * - * @param manga the manga to bind. + * @param item the manga item to bind. */ - override fun onSetValues(manga: Manga) { + override fun onSetValues(item: LibraryItem) { // Update the title of the manga. - itemView.title.text = manga.title + itemView.title.text = item.manga.title // Update the unread count and its visibility. with(itemView.unread_text) { - visibility = if (manga.unread > 0) View.VISIBLE else View.GONE - text = manga.unread.toString() + visibility = if (item.manga.unread > 0) View.VISIBLE else View.GONE + text = item.manga.unread.toString() + } + // Update the download count and its visibility. + with(itemView.download_text) { + visibility = if (item.downloadCount > 0) View.VISIBLE else View.GONE + text = "${item.downloadCount}" + } + //show local text badge if local manga + with(itemView.local_text) { + visibility = if (item.manga.source == LocalSource.ID) View.VISIBLE else View.GONE } // Create thumbnail onclick to simulate long click @@ -46,14 +54,14 @@ class LibraryListHolder( } // Update the cover. - Glide.clear(itemView.thumbnail) - Glide.with(itemView.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) + GlideApp.with(itemView.context).clear(itemView.thumbnail) + GlideApp.with(itemView.context) + .load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() - .bitmapTransform(CropCircleTransformation(itemView.context)) + .circleCrop() .dontAnimate() .into(itemView.thumbnail) } -} \ No newline at end of file +} 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 index 6c1d4a040..e79503a7d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt @@ -25,7 +25,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A /** * List of groups shown in the view. */ - private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup()) + private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup(), BadgeGroup()) /** * Adapter instance. @@ -117,7 +117,9 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A private val unread = Item.MultiSort(R.string.action_filter_unread, this) - override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total) + private val source = Item.MultiSort(R.string.manga_info_source_label, this) + + override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total, source) override val header = Item.Header(R.string.action_sort) @@ -133,6 +135,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A 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 + source.state = if (sorting == LibrarySort.SOURCE) order else SORT_NONE } override fun onItemClicked(item: Item) { @@ -153,6 +156,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A lastUpdated -> LibrarySort.LAST_UPDATED unread -> LibrarySort.UNREAD total -> LibrarySort.TOTAL + source -> LibrarySort.SOURCE else -> throw Exception("Unknown sorting") }) preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false) @@ -162,6 +166,23 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A } + 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. */ 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 7661f218f..44d5f2522 100755 --- 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,7 +1,6 @@ package eu.kanade.tachiyomi.ui.library import android.os.Bundle -import android.util.Pair import com.hippo.unifile.UniFile import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.tachiyomi.data.cache.CoverCache @@ -29,6 +28,16 @@ import java.io.IOException import java.io.InputStream import java.util.* +/** + * 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]. */ @@ -53,6 +62,11 @@ class LibraryPresenter( */ private val filterTriggerRelay = BehaviorRelay.create(Unit) + /** + * Relay used to apply the UI update to the last emission of the library. + */ + private val downloadTriggerRelay = BehaviorRelay.create(Unit) + /** * Relay used to apply the selected sorting method to the last emission of the library. */ @@ -74,14 +88,15 @@ class LibraryPresenter( fun subscribeLibrary() { if (librarySubscription.isNullOrUnsubscribed()) { librarySubscription = getLibraryObservable() + .combineLatest(downloadTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> lib.apply { setDownloadCount(mangaMap) } }) .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), - { lib, _ -> Pair(lib.first, applyFilters(lib.second)) }) + { lib, _ -> lib.copy(mangaMap = applyFilters(lib.mangaMap)) }) .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), - { lib, _ -> Pair(lib.first, applySort(lib.second)) }) - .map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) } + { lib, _ -> lib.copy(mangaMap = applySort(lib.mangaMap)) }) .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, pair -> - view.onNextLibraryUpdate(pair.first, pair.second) + .subscribeLatestCache({ view, (categories, mangaMap) -> + view.onNextLibraryUpdate(categories, mangaMap) }) } } @@ -91,7 +106,7 @@ class LibraryPresenter( * * @param map the map to filter. */ - private fun applyFilters(map: Map>): Map> { + private fun applyFilters(map: LibraryMap): LibraryMap { // Cached list of downloaded manga directories given a source id. val mangaDirsForSource = mutableMapOf>() @@ -104,31 +119,36 @@ class LibraryPresenter( val filterCompleted = preferences.filterCompleted().getOrDefault() - val filterFn: (Manga) -> Boolean = f@ { manga -> + val filterFn: (LibraryItem) -> Boolean = f@ { item -> // Filter out manga without source. - val source = sourceManager.get(manga.source) ?: return@f false + val source = sourceManager.get(item.manga.source) ?: return@f false // Filter when there isn't unread chapters. - if (filterUnread && manga.unread == 0) { + if (filterUnread && item.manga.unread == 0) { return@f false } - if (filterCompleted && manga.status != SManga.COMPLETED) { + if (filterCompleted && item.manga.status != SManga.COMPLETED) { return@f false } // Filter when the download directory doesn't exist or is null. if (filterDownloaded) { + // Don't bother with directory checking if download count has been set. + if (item.downloadCount != -1) { + return@f item.downloadCount > 0 + } + // Get the directories for the source of the manga. val dirsForSource = mangaDirsForSource.getOrPut(source.id) { val sourceDir = downloadManager.findSourceDir(source) sourceDir?.listFiles()?.associateBy { it.name }.orEmpty() } - val mangaDirName = downloadManager.getMangaDirName(manga) + val mangaDirName = downloadManager.getMangaDirName(item.manga) val mangaDir = dirsForSource[mangaDirName] ?: return@f false - val hasDirs = chapterDirectories.getOrPut(manga.id!!) { + val hasDirs = chapterDirectories.getOrPut(item.manga.id!!) { mangaDir.listFiles()?.isNotEmpty() ?: false } if (!hasDirs) { @@ -141,12 +161,57 @@ class LibraryPresenter( return map.mapValues { entry -> entry.value.filter(filterFn) } } + /** + * Sets downloaded chapter count to each manga. + * + * @param map the map of manga. + */ + private fun setDownloadCount(map: LibraryMap) { + if (!preferences.downloadBadge().getOrDefault()) { + // Unset download count if the preference is not enabled. + for ((_, itemList) in map) { + for (item in itemList) { + item.downloadCount = -1 + } + } + return + } + + // Cached list of downloaded manga directories given a source id. + val mangaDirsForSource = mutableMapOf>() + + // Cached list of downloaded chapter directories for a manga. + val chapterDirectories = mutableMapOf() + + val downloadCountFn: (LibraryItem) -> Int = f@ { item -> + val source = sourceManager.get(item.manga.source) ?: return@f 0 + + // Get the directories for the source of the manga. + val dirsForSource = mangaDirsForSource.getOrPut(source.id) { + val sourceDir = downloadManager.findSourceDir(source) + sourceDir?.listFiles()?.associateBy { it.name }.orEmpty() + } + val mangaDirName = downloadManager.getMangaDirName(item.manga) + val mangaDir = dirsForSource[mangaDirName] ?: return@f 0 + + chapterDirectories.getOrPut(item.manga.id!!) { + mangaDir.listFiles()?.size ?: 0 + } + } + + for ((_, itemList) in map) { + for (item in itemList) { + item.downloadCount = downloadCountFn(item) + } + } + } + /** * Applies library sorting to the given map of manga. * * @param map the map to sort. */ - private fun applySort(map: Map>): Map> { + private fun applySort(map: LibraryMap): LibraryMap { val sortingMode = preferences.librarySortingMode().getOrDefault() val lastReadManga by lazy { @@ -158,22 +223,27 @@ class LibraryPresenter( db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ } } - val sortFn: (Manga, Manga) -> Int = { manga1, manga2 -> + val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> when (sortingMode) { - LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title) + LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title) LibrarySort.LAST_READ -> { // Get index of manga, set equal to list if size unknown. - val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size - val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size + val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size + val manga2LastRead = lastReadManga[i2.manga.id!!] ?: lastReadManga.size manga1LastRead.compareTo(manga2LastRead) } - LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update) - LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread) + LibrarySort.LAST_UPDATED -> i2.manga.last_update.compareTo(i1.manga.last_update) + LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread) LibrarySort.TOTAL -> { - val manga1TotalChapter = totalChapterManga[manga1.id!!] ?: 0 - val mange2TotalChapter = totalChapterManga[manga2.id!!] ?: 0 + val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0 + val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0 manga1TotalChapter.compareTo(mange2TotalChapter) } + LibrarySort.SOURCE -> { + val source1Name = sourceManager.get(i1.manga.source)?.name ?: "" + val source2Name = sourceManager.get(i2.manga.source)?.name ?: "" + source1Name.compareTo(source2Name) + } else -> throw Exception("Unknown sorting mode") } } @@ -191,7 +261,7 @@ class LibraryPresenter( * * @return an observable of the categories and its manga. */ - private fun getLibraryObservable(): Observable, Map>>> { + private fun getLibraryObservable(): Observable { return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), { dbCategories, libraryManga -> val categories = if (libraryManga.containsKey(0)) @@ -200,7 +270,7 @@ class LibraryPresenter( dbCategories this.categories = categories - Pair(categories, libraryManga) + Library(categories, libraryManga) }) } @@ -219,9 +289,12 @@ class LibraryPresenter( * @return an observable containing a map with the category id as key and a list of manga as the * value. */ - private fun getLibraryMangasObservable(): Observable>> { + private fun getLibraryMangasObservable(): Observable { + val libraryAsList = preferences.libraryAsList() return db.getLibraryMangas().asRxObservable() - .map { list -> list.groupBy { it.category } } + .map { list -> + list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category } + } } /** @@ -231,6 +304,13 @@ class LibraryPresenter( filterTriggerRelay.call(Unit) } + /** + * Requests the library to have download badges added. + */ + fun requestDownloadBadgesUpdate() { + downloadTriggerRelay.call(Unit) + } + /** * Requests the library to be sorted. */ 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 677eeb244..57c9f28b1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -7,4 +7,5 @@ object LibrarySort { const val LAST_UPDATED = 2 const val UNREAD = 3 const val TOTAL = 4 + const val SOURCE = 5 } \ No newline at end of file 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 a9ff2b33c..c0fd90c89 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -23,9 +23,8 @@ 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.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController import eu.kanade.tachiyomi.ui.download.DownloadController -import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController @@ -97,11 +96,10 @@ class MainActivity : BaseActivity() { 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_latest_updates -> setRoot(LatestUpdatesController(), id) - // --> EH + R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id) + // --> EXH R.id.nav_drawer_batch_add -> setRoot(BatchAddController(), id) - // <-- EH + // <-- EHX R.id.nav_drawer_downloads -> { router.pushController(RouterTransaction.with(DownloadController()) .pushChangeHandler(FadeChangeHandler()) @@ -117,7 +115,7 @@ class MainActivity : BaseActivity() { true } - val container = findViewById(R.id.controller_container) as ViewGroup + val container: ViewGroup = findViewById(R.id.controller_container) router = Conductor.attachRouter(this, container, savedInstanceState) if (!router.hasRootController()) { @@ -347,4 +345,4 @@ class MainActivity : BaseActivity() { const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA" } -} \ No newline at end of file +} 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 b5ef25f1a..936763bf7 100755 --- 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 @@ -6,7 +6,9 @@ import eu.davidea.viewholders.FlexibleViewHolder 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.util.getResourceColor import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.setVectorCompat import kotlinx.android.synthetic.main.chapters_item.view.* import java.util.* @@ -33,6 +35,9 @@ class ChapterHolder( else -> chapter.name } + // Set the correct drawable for dropdown and update the tint to match theme. + view.chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) + // Set correct text color chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) 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 d8afdca68..d8639cfb8 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 @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.manga.chapter -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.kanade.tachiyomi.R @@ -27,11 +26,8 @@ class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem return R.layout.chapters_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, - inflater: LayoutInflater, - parent: ViewGroup): ChapterHolder { - - return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ChapterHolder { + return ChapterHolder(view, adapter as ChaptersAdapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, 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 index ab72549e6..f22b57613 100755 --- 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 @@ -29,9 +29,9 @@ class ChaptersAdapter( val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) - override fun updateDataSet(items: List) { - this.items = items - super.updateDataSet(items.toList()) + override fun updateDataSet(items: List?) { + this.items = items ?: emptyList() + super.updateDataSet(items) } fun indexOf(item: ChapterItem): Int { 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 index a73fe5f19..630096512 100644 --- 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 @@ -14,6 +14,7 @@ import android.view.* 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 @@ -79,9 +80,7 @@ class ChaptersController : NucleusController(), recycler.layoutManager = LinearLayoutManager(context) recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) recycler.setHasFixedSize(true) - // TODO enable in a future commit -// adapter.setFastScroller(fast_scroller, context.getResourceColor(R.attr.colorAccent)) -// adapter.toggleFastScroller() + adapter?.fastScroller = view.fast_scroller swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } @@ -247,7 +246,7 @@ class ChaptersController : NucleusController(), override fun onItemClick(position: Int): Boolean { val adapter = adapter ?: return false val item = adapter.getItem(position) ?: return false - if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { toggleSelection(position) return true } else { @@ -277,7 +276,7 @@ class ChaptersController : NucleusController(), fun getSelectedChapters(): List { val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.map { adapter.getItem(it) } + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } } fun createActionModeIfNeeded() { @@ -292,7 +291,7 @@ class ChaptersController : NucleusController(), override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.chapter_selection, menu) - adapter?.mode = FlexibleAdapter.MODE_MULTI + adapter?.mode = SelectableAdapter.Mode.MULTI return true } @@ -320,7 +319,7 @@ class ChaptersController : NucleusController(), } override fun onDestroyActionMode(mode: ActionMode) { - adapter?.mode = FlexibleAdapter.MODE_SINGLE + adapter?.mode = SelectableAdapter.Mode.SINGLE adapter?.clearSelection() selectedItems.clear() actionMode = null 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 index 71283739d..17ca3e055 100755 --- 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 @@ -7,7 +7,6 @@ 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.DownloadService import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.Source @@ -35,8 +34,6 @@ class ChaptersPresenter( private val downloadManager: DownloadManager = Injekt.get() ) : BasePresenter() { - private val context = preferences.context - /** * List of chapters of the manga. It's always unfiltered and unsorted. */ @@ -246,7 +243,6 @@ class ChaptersPresenter( * @param chapters the list of chapters to download. */ fun downloadChapters(chapters: List) { - DownloadService.start(context) downloadManager.downloadChapters(manga, chapters) } 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 index d1febd2d5..faf0132a3 100644 --- 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 @@ -1,26 +1,35 @@ package eu.kanade.tachiyomi.ui.manga.info +import android.app.Dialog +import android.app.PendingIntent import android.content.Intent import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.net.Uri +import android.os.Build import android.os.Bundle import android.support.customtabs.CustomTabsIntent +import android.support.v4.content.pm.ShortcutInfoCompat +import android.support.v4.content.pm.ShortcutManagerCompat +import android.support.v4.graphics.drawable.IconCompat import android.view.* import com.afollestad.materialdialogs.MaterialDialog -import com.bumptech.glide.BitmapRequestBuilder -import com.bumptech.glide.BitmapTypeRequest -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.target.SimpleTarget +import com.bumptech.glide.request.transition.Transition import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.view.clicks 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.glide.GlideApp +import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.main.MainActivity @@ -28,15 +37,9 @@ import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.toast -import jp.wasabeef.glide.transformations.CropCircleTransformation import jp.wasabeef.glide.transformations.CropSquareTransformation import jp.wasabeef.glide.transformations.MaskTransformation -import jp.wasabeef.glide.transformations.RoundedCornersTransformation import kotlinx.android.synthetic.main.manga_info_controller.view.* -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import rx.subscriptions.Subscriptions import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat @@ -152,16 +155,16 @@ class MangaInfoController : NucleusController(), // Set cover if it wasn't already. if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(context) + GlideApp.with(context) .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() .into(manga_cover) if (backdrop != null) { - Glide.with(context) + GlideApp.with(context) .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() .into(backdrop) } @@ -181,7 +184,7 @@ class MangaInfoController : NucleusController(), /** * Toggles the favorite status and asks for confirmation to delete downloaded chapters. */ - fun toggleFavorite() { + private fun toggleFavorite() { val view = view val isNowFavorite = presenter.toggleFavorite() @@ -197,7 +200,7 @@ class MangaInfoController : NucleusController(), /** * Open the manga in browser. */ - fun openInBrowser() { + private fun openInBrowser() { val context = view?.context ?: return val source = presenter.source as? HttpSource ?: return @@ -288,18 +291,19 @@ class MangaInfoController : NucleusController(), if (manga.favorite) { val categories = presenter.getCategories() val defaultCategory = categories.find { it.id == preferences.defaultCategory() } - if (defaultCategory != null) { - presenter.moveMangaToCategory(manga, defaultCategory) - } else if (categories.size <= 1) { // default or the one from the user - presenter.moveMangaToCategory(manga, categories.firstOrNull()) - } else { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() + when { + defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) + categories.size <= 1 -> // default or the one from the user + presenter.moveMangaToCategory(manga, categories.firstOrNull()) + 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) + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } } } } @@ -310,93 +314,114 @@ class MangaInfoController : NucleusController(), } /** - * Add the manga to the home screen + * Add a shortcut of the manga to the home screen */ - fun addToHomeScreen() { + 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.Builder(activity!!) + .title(R.string.icon_shape) + .negativeText(android.R.string.cancel) + .items(modes.map { activity?.getString(it) }) + .itemsCallback { _, _, i, _ -> + (targetController as? MangaInfoController)?.createShortcutForShape(i) + } + .build() + } + } + + /** + * 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) { + 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 : SimpleTarget(96, 96) { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + createShortcut(resource) + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + activity?.toast(R.string.icon_creation_fail) + } + }) + } + + /** + * 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)) - val addIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT") - .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) + // Check if shortcut placement is supported + if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) { + val shortcutId = "manga-shortcut-${presenter.manga.title}-${presenter.source.name}" - //Set shortcut title - val dialog = MaterialDialog.Builder(activity) - .title(R.string.shortcut_title) - .input("", presenter.manga.title, { _, text -> - //Set shortcut title - addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString()) + // Create shortcut info + val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId) + .setShortLabel(presenter.manga.title) + .setIcon(IconCompat.createWithBitmap(icon)) + .setIntent(shortcutIntent) + .build() - reshapeIconBitmap(addIntent, - Glide.with(activity).load(presenter.manga).asBitmap()) - }) - .negativeText(android.R.string.cancel) - .show() + val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create the CallbackIntent. + val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo) - untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() }) - } - - fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest) { - val activity = activity ?: return - - val modes = intArrayOf(R.string.circular_icon, - R.string.rounded_icon, - R.string.square_icon, - R.string.star_icon) - - fun BitmapRequestBuilder.toIcon(): Bitmap { - return this.into(96, 96).get() - } - - // i = 0: Circular icon - // i = 1: Rounded icon - // i = 2: Square icon - // i = 3: Star icon (because boredom) - fun getIcon(i: Int): Bitmap? { - return when (i) { - 0 -> request.transform(CropCircleTransformation(activity)).toIcon() - 1 -> request.transform(RoundedCornersTransformation(activity, 5, 0)).toIcon() - 2 -> request.transform(CropSquareTransformation(activity)).toIcon() - 3 -> request.transform(CenterCrop(activity), - MaskTransformation(activity, R.drawable.mask_star)).toIcon() - else -> null + // 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) } - - val dialog = MaterialDialog.Builder(activity) - .title(R.string.icon_shape) - .negativeText(android.R.string.cancel) - .items(modes.map { activity.getString(it) }) - .itemsCallback { _, _, i, _ -> - Observable.fromCallable { getIcon(i) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ icon -> - if (icon != null) createShortcut(addIntent, icon) - }, { - activity.toast(R.string.icon_creation_fail) - }) - } - .show() - - untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() }) } - fun createShortcut(addIntent: Intent, icon: Bitmap) { - val activity = activity ?: return - - //Send shortcut intent - addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon) - activity.sendBroadcast(addIntent) - //Go to launcher to show this shiny new shortcut! - val startMain = Intent(Intent.ACTION_MAIN) - startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(startMain) - } - -} +} \ No newline at end of file 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 6b1bf0a22..249d96562 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 @@ -43,7 +43,7 @@ class SetTrackChaptersDialog : DialogController val view = dialog.customView if (view != null) { // Remove focus to update selected number - val np = view.findViewById(R.id.chapters_picker) as NumberPicker + val np: NumberPicker = view.findViewById(R.id.chapters_picker) np.clearFocus() (targetController as? Listener)?.setChaptersRead(item, np.value) @@ -53,7 +53,7 @@ class SetTrackChaptersDialog : DialogController val view = dialog.customView if (view != null) { - val np = view.findViewById(R.id.chapters_picker) as NumberPicker + 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 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 80931940a..44734f64b 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 @@ -43,7 +43,7 @@ class SetTrackScoreDialog : DialogController val view = dialog.customView if (view != null) { // Remove focus to update selected number - val np = view.findViewById(R.id.score_picker) as NumberPicker + val np: NumberPicker = view.findViewById(R.id.score_picker) np.clearFocus() (targetController as? Listener)?.setScore(item, np.value) @@ -53,7 +53,7 @@ class SetTrackScoreDialog : DialogController val view = dialog.customView if (view != null) { - val np = view.findViewById(R.id.score_picker) as NumberPicker + val np: NumberPicker = view.findViewById(R.id.score_picker) val scores = item.service.getScoreList().toTypedArray() np.maxValue = scores.size - 1 np.displayedValues = scores 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 index b9e152ba4..bac625de3 100755 --- 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 @@ -67,7 +67,7 @@ class TrackPresenter( .toList() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> view.onRefreshDone() }, + .subscribeFirst({ view, _ -> view.onRefreshDone() }, TrackController::onRefreshError) } @@ -99,7 +99,7 @@ class TrackPresenter( .flatMap { db.insertTrack(track).asRxObservable() } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> view.onRefreshDone() }, + .subscribeFirst({ view, _ -> view.onRefreshDone() }, { view, error -> view.onRefreshError(error) 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 new file mode 100644 index 000000000..51527cbda --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.support.v7.widget.AppCompatTextView +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ScaleXSpan +import android.util.AttributeSet +import android.widget.TextView + +class PageIndicatorTextView(context: Context, attrs: AttributeSet? = null) : + AppCompatTextView(context, attrs) { + + private val fillColor = Color.rgb(235, 235, 235) + private val strokeColor = Color.rgb(45, 45, 45) + + override fun onDraw(canvas: Canvas) { + textColorField.set(this, strokeColor) + paint.strokeWidth = 4f + paint.style = Paint.Style.STROKE + super.onDraw(canvas) + + textColorField.set(this, fillColor) + paint.strokeWidth = 0f + paint.style = Paint.Style.FILL + super.onDraw(canvas) + } + + @SuppressLint("SetTextI18n") + override fun setText(text: CharSequence?, type: BufferType?) { + // Add spaces at the start & end of the text, otherwise the stroke is cut-off because it's + // not taken into account when measuring the text (view's padding doesn't help). + val currText = " $text " + + // Also add a bit of spacing between each character, as the stroke overlaps them + val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")) + + for (i in 1..finalText.lastIndex step 2) { + finalText.setSpan(ScaleXSpan(0.1f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + super.setText(finalText, TextView.BufferType.SPANNABLE) + } + + private companion object { + // We need to use reflection to set the text color instead of using [setTextColor], + // otherwise the view is invalidated inside [onDraw] and there's an infinite loop + val textColorField = TextView::class.java.getDeclaredField("mCurTextColor").apply { + isAccessible = true + }!! + } +} \ No newline at end of file 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 d49bba416..95cf15539 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -8,7 +8,6 @@ import android.graphics.Color import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Bundle -import android.support.v4.content.ContextCompat import android.view.* import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS import android.view.animation.Animation @@ -27,10 +26,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonReader -import eu.kanade.tachiyomi.util.GLUtil -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.plusAssign -import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.widget.SimpleAnimationListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener import kotlinx.android.synthetic.main.reader_activity.* @@ -42,6 +38,7 @@ import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription import timber.log.Timber import uy.kohesive.injekt.injectLazy +import java.io.File import java.text.DecimalFormat import java.util.concurrent.TimeUnit @@ -178,8 +175,8 @@ class ReaderActivity : BaseRxActivity() { .content(getString(R.string.confirm_update_manga_sync, chapterToUpdate)) .positiveText(android.R.string.yes) .negativeText(android.R.string.no) - .onPositive { dialog, which -> presenter.updateTrackLastChapterRead(chapterToUpdate) } - .onAny { dialog1, which1 -> super.onBackPressed() } + .onPositive { _, _ -> presenter.updateTrackLastChapterRead(chapterToUpdate) } + .onAny { _, _ -> super.onBackPressed() } .show() } else { presenter.updateTrackLastChapterRead(chapterToUpdate) @@ -239,7 +236,7 @@ class ReaderActivity : BaseRxActivity() { .title(getString(R.string.options)) .items(R.array.reader_image_options) .itemsIds(R.array.reader_image_options_values) - .itemsCallback { materialDialog, view, i, charSequence -> + .itemsCallback { _, _, i, _ -> when (i) { 0 -> setImageAsCover(page) 1 -> shareImage(page) @@ -384,7 +381,7 @@ class ReaderActivity : BaseRxActivity() { private fun initializeBottomMenu() { // Intercept all events in this layout - reader_menu_bottom.setOnTouchListener { v, event -> true } + reader_menu_bottom.setOnTouchListener { _, _ -> true } page_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() { override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { @@ -519,12 +516,8 @@ class ReaderActivity : BaseRxActivity() { val rootView = window.decorView.rootView if (theme == BLACK_THEME) { rootView.setBackgroundColor(Color.BLACK) - page_number.setTextColor(ContextCompat.getColor(this, R.color.textColorPrimaryDark)) - page_number.setBackgroundColor(ContextCompat.getColor(this, R.color.pageNumberBackgroundDark)) } else { rootView.setBackgroundColor(Color.WHITE) - page_number.setTextColor(ContextCompat.getColor(this, R.color.textColorPrimaryLight)) - page_number.setBackgroundColor(ContextCompat.getColor(this, R.color.pageNumberBackgroundLight)) } } @@ -576,8 +569,12 @@ class ReaderActivity : BaseRxActivity() { if (page.status != Page.READY) return + var uri = page.uri ?: return + if (uri.toString().startsWith("file://")) { + uri = File(uri.toString().substringAfter("file://")).getUriCompat(this) + } val intent = Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, page.uri) + putExtra(Intent.EXTRA_STREAM, uri) flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION type = "image/*" } @@ -597,7 +594,7 @@ class ReaderActivity : BaseRxActivity() { .content(getString(R.string.confirm_set_image_as_cover)) .positiveText(android.R.string.yes) .negativeText(android.R.string.no) - .onPositive { dialog, which -> presenter.setImageAsCover(page) } + .onPositive { _, _ -> presenter.setImageAsCover(page) } .show() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt index 973869031..7e3ac1003 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderCustomFilterDialog.kt @@ -106,12 +106,12 @@ class ReaderCustomFilterDialog : DialogFragment() { // Set listeners switch_color_filter.isChecked = preferences.colorFilter().getOrDefault() - switch_color_filter.setOnCheckedChangeListener { v, isChecked -> + switch_color_filter.setOnCheckedChangeListener { _, isChecked -> preferences.colorFilter().set(isChecked) } custom_brightness.isChecked = preferences.customBrightness().getOrDefault() - custom_brightness.setOnCheckedChangeListener { v, isChecked -> + custom_brightness.setOnCheckedChangeListener { _, isChecked -> preferences.customBrightness().set(isChecked) } 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 2a29a6cd9..02a90c3f3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -154,7 +154,7 @@ class ReaderPresenter( restartableLatestCache(LOAD_ACTIVE_CHAPTER, { loadChapterObservable(chapter) }, - { view, chapter -> view.onChapterReady(this.chapter) }, + { view, _ -> view.onChapterReady(this.chapter) }, { view, error -> view.onChapterError(error) }) if (savedState == null) { @@ -315,7 +315,7 @@ class ReaderPresenter( .observeOn(AndroidSchedulers.mainThread()) .subscribeLatestCache({ view, chapter -> view.onAppendChapter(chapter) - }, { view, error -> + }, { view, _ -> view.onChapterAppendError() }) } @@ -328,9 +328,10 @@ class ReaderPresenter( fun retryPage(page: Page?) { if (page != null && source is HttpSource) { page.status = Page.QUEUE - val uri = page.uri - if (uri != null && !page.chapter.isDownloaded) { - chapterCache.removeFileFromCache(uri.encodedPath.substringAfterLast('/')) + val imageUrl = page.imageUrl + if (imageUrl != null && !page.chapter.isDownloaded) { + val key = DiskUtil.hashKeyForDisk(page.url) + chapterCache.removeFileFromCache(key) } //If we are using EHentai/ExHentai, get a new image URL diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt index 32e6e6d34..fa09cc120 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt @@ -75,17 +75,17 @@ class ReaderSettingsDialog : DialogFragment() { background_color.setSelection(preferences.readerTheme().getOrDefault(), false) show_page_number.isChecked = preferences.showPageNumber().getOrDefault() - show_page_number.setOnCheckedChangeListener { v, isChecked -> + show_page_number.setOnCheckedChangeListener { _, isChecked -> preferences.showPageNumber().set(isChecked) } fullscreen.isChecked = preferences.fullscreen().getOrDefault() - fullscreen.setOnCheckedChangeListener { v, isChecked -> + fullscreen.setOnCheckedChangeListener { _, isChecked -> preferences.fullscreen().set(isChecked) } crop_borders.isChecked = preferences.cropBorders().getOrDefault() - crop_borders.setOnCheckedChangeListener { v, isChecked -> + crop_borders.setOnCheckedChangeListener { _, isChecked -> preferences.cropBorders().set(isChecked) } } 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 9f4f43bd9..ab690c819 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt @@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.reader import android.content.Context import android.graphics.Bitmap import android.support.v4.app.NotificationCompat -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy -import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.notificationManager import java.io.File @@ -19,13 +19,13 @@ class SaveImageNotifier(private val context: Context) { /** * Notification builder. */ - private val notificationBuilder = NotificationCompat.Builder(context) + private val notificationBuilder = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON) /** * Id of the notification. */ private val notificationId: Int - get() = Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID + get() = Notifications.ID_DOWNLOAD_IMAGE /** * Called when image download/copy is complete. This method must be called in a background @@ -34,12 +34,12 @@ class SaveImageNotifier(private val context: Context) { * @param file image file containing downloaded page image. */ fun onComplete(file: File) { - val bitmap = Glide.with(context) - .load(file) + val bitmap = GlideApp.with(context) .asBitmap() + .load(file) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) - .into(720, 1280) + .submit(720, 1280) .get() if (bitmap != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt index 6dcc2393e..1b0fef4ce 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt @@ -70,7 +70,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? setBitmapDecoderClass(reader.bitmapDecoderClass) setVerticalScrollingParent(reader is VerticalReader) setCropBorders(reader.cropBorders) - setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) } + setOnTouchListener { _, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) } setOnLongClickListener { reader.onLongClick(page) } setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { override fun onReady() { @@ -83,7 +83,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? }) } - retry_button.setOnTouchListener { v, event -> + retry_button.setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_UP) { activity.presenter.retryPage(page) } @@ -130,7 +130,11 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) .subscribe { progress -> - progress_text.text = context.getString(R.string.download_progress, progress) + progress_text.text = if (progress > 0) { + context.getString(R.string.download_progress, progress) + } else { + context.getString(R.string.downloading) + } } } 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 373902027..6871e6bab 100755 --- 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 @@ -22,7 +22,7 @@ class WebtoonAdapter(val fragment: WebtoonReader) : RecyclerView.Adapter fragment.imageGestureDetector.onTouchEvent(ev) } + val touchListener = View.OnTouchListener { _, ev -> fragment.imageGestureDetector.onTouchEvent(ev) } /** * Returns the number of pages. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt index 5b814db1e..39958bd98 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt @@ -150,7 +150,11 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) .subscribe { progress -> - view.progress_text.text = view.context.getString(R.string.download_progress, progress) + view.progress_text.text = if (progress > 0) { + view.context.getString(R.string.download_progress, progress) + } else { + view.context.getString(R.string.downloading) + } } addSubscription(progressSubscription) 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 de64e85b0..a766613ad 100755 --- 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 @@ -1,9 +1,7 @@ package eu.kanade.tachiyomi.ui.recent_updates import android.text.format.DateUtils -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.TextView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractHeaderItem @@ -17,8 +15,8 @@ class DateItem(val date: Date) : AbstractHeaderItem() { return R.layout.recent_chapters_section_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { - return Holder(inflater.inflate(layoutRes, parent, false), adapter) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return Holder(view, adapter) } override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List?) { @@ -41,7 +39,7 @@ class DateItem(val date: Date) : AbstractHeaderItem() { private val now = Date().time - val section_text = view.findViewById(R.id.section_text) as TextView + 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) 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 434ee400c..37a72f537 100755 --- 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 @@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.recent_updates import android.view.View import android.widget.PopupMenu -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.util.getResourceColor -import jp.wasabeef.glide.transformations.CropCircleTransformation +import eu.kanade.tachiyomi.util.setVectorCompat import kotlinx.android.synthetic.main.recent_chapters_item.view.* /** @@ -63,13 +63,16 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // Set manga title view.manga_title.text = item.manga.title + // Set the correct drawable for dropdown and update the tint to match theme. + view.chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) + // Set cover - Glide.clear(itemView.manga_cover) + GlideApp.with(itemView.context).clear(itemView.manga_cover) if (!item.manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(itemView.context) + GlideApp.with(itemView.context) .load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .bitmapTransform(CropCircleTransformation(view.context)) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .circleCrop() .into(itemView.manga_cover) } 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 a98287d78..eb983c608 100755 --- 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 @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.recent_updates -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.kanade.tachiyomi.R @@ -27,11 +26,7 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem return R.layout.recent_chapters_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, - inflater: LayoutInflater, - parent: ViewGroup): RecentChapterHolder { - - val view = inflater.inflate(layoutRes, parent, false) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): RecentChapterHolder { return RecentChapterHolder(view , adapter as RecentChaptersAdapter) } 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 e54a24beb..b8b684481 100755 --- 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 @@ -11,6 +11,7 @@ import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges 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.model.Download @@ -120,7 +121,7 @@ class RecentChaptersController : NucleusController(), // Get item from position val item = adapter.getItem(position) as? RecentChapterItem ?: return false - if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { toggleSelection(position) return true } else { @@ -175,7 +176,7 @@ class RecentChaptersController : NucleusController(), */ fun onNextRecentChapters(chapters: List>) { destroyActionModeIfNeeded() - adapter?.updateDataSet(chapters.toMutableList()) + adapter?.updateDataSet(chapters) } override fun onUpdateEmptyView(size: Int) { @@ -295,7 +296,7 @@ class RecentChaptersController : NucleusController(), */ override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) - adapter?.mode = FlexibleAdapter.MODE_MULTI + adapter?.mode = SelectableAdapter.Mode.MULTI return true } @@ -332,7 +333,7 @@ class RecentChaptersController : NucleusController(), * @param mode the ActionMode object */ override fun onDestroyActionMode(mode: ActionMode?) { - adapter?.mode = FlexibleAdapter.MODE_IDLE + 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 fe7cebfb5..3564a8ba1 100755 --- 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 @@ -5,7 +5,6 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.MangaChapter 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.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.SourceManager @@ -25,8 +24,6 @@ class RecentChaptersPresenter( private val sourceManager: SourceManager = Injekt.get() ) : BasePresenter() { - private val context = preferences.context - /** * List containing chapter and manga information */ @@ -60,7 +57,9 @@ class RecentChaptersPresenter( // 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) }) + val byDay = mangaChapters + .filter { sourceManager.get(it.manga.source) != null } + .groupByTo(map, { getMapKey(it.chapter.date_fetch) }) byDay.flatMap { val dateItem = DateItem(it.key) it.value.map { RecentChapterItem(it.chapter, it.manga, dateItem) } @@ -207,7 +206,6 @@ class RecentChaptersPresenter( */ fun downloadChapters(items: List) { items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) } - DownloadService.start(context) } /** 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 6410f1e53..5f0f98864 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 @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.recently_read import android.os.Bundle import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -77,7 +76,7 @@ class RecentlyReadController : NucleusController(), * @param mangaHistory list of manga history */ fun onNextManga(mangaHistory: List) { - adapter?.updateDataSet(mangaHistory.toList()) + adapter?.updateDataSet(mangaHistory) } override fun onUpdateEmptyView(size: Int) { @@ -91,10 +90,7 @@ class RecentlyReadController : NucleusController(), override fun onResumeClick(position: Int) { val activity = activity ?: return - val adapter = adapter ?: return - if (position == RecyclerView.NO_POSITION) return - - val (manga, chapter, _) = adapter.getItem(position).mch + val (manga, chapter, _) = adapter?.getItem(position)?.mch ?: return val nextChapter = presenter.getNextChapter(chapter, manga) if (nextChapter != null) { @@ -106,11 +102,7 @@ class RecentlyReadController : NucleusController(), } override fun onRemoveClick(position: Int) { - val adapter = adapter ?: return - if (position == RecyclerView.NO_POSITION) return - - val (manga, _, history) = adapter.getItem(position).mch - + val (manga, _, history) = adapter?.getItem(position)?.mch ?: return RemoveHistoryDialog(this, manga, history).showDialog(router) } 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 2d4464ffa..ecabfb873 100755 --- 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,11 +1,11 @@ package eu.kanade.tachiyomi.ui.recently_read import android.view.View -import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.data.glide.GlideApp import kotlinx.android.synthetic.main.recently_read_item.view.* import java.util.* @@ -58,15 +58,15 @@ class RecentlyReadHolder( itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read)) // Set cover - Glide.clear(itemView.cover) + GlideApp.with(itemView.context).clear(itemView.cover) if (!manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(itemView.context) + GlideApp.with(itemView.context) .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() .into(itemView.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 7ec7b0db4..8faf640c6 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 @@ -1,12 +1,10 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.util.inflate class RecentlyReadItem(val mch: MangaChapterHistory) : AbstractFlexibleItem() { @@ -14,11 +12,7 @@ class RecentlyReadItem(val mch: MangaChapterHistory) : AbstractFlexibleItem, - inflater: LayoutInflater, - parent: ViewGroup): RecentlyReadHolder { - - val view = parent.inflate(layoutRes) + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): RecentlyReadHolder { return RecentlyReadHolder(view, adapter as RecentlyReadAdapter) } 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 28abe7881..934869103 100755 --- 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 @@ -5,6 +5,7 @@ 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.source.SourceManager import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import rx.Observable import rx.android.schedulers.AndroidSchedulers @@ -23,6 +24,8 @@ class RecentlyReadPresenter : BasePresenter() { */ val db: DatabaseHelper by injectLazy() + private val sourceManager: SourceManager by injectLazy() + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -42,7 +45,10 @@ class RecentlyReadPresenter : BasePresenter() { cal.add(Calendar.MONTH, -1) return db.getRecentManga(cal.time).asRxObservable() - .map { it.map(::RecentlyReadItem) } + .map { recents -> + recents.filter { sourceManager.get(it.manga.source) != null } + .map(::RecentlyReadItem) + } .observeOn(AndroidSchedulers.mainThread()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt index af0b6ffc7..982e74fe1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt @@ -30,7 +30,7 @@ class AnilistLoginActivity : AppCompatActivity() { .observeOn(AndroidSchedulers.mainThread()) .subscribe({ returnToSettings() - }, { error -> + }, { _ -> returnToSettings() }) } else { 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 3aa9398e9..1a2f6c458 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 @@ -22,6 +22,9 @@ import java.text.ParseException import java.text.SimpleDateFormat import java.util.* import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys +import android.content.Intent +import android.net.Uri + class SettingsAboutController : SettingsController() { @@ -66,6 +69,15 @@ class SettingsAboutController : SettingsController() { isVisible = false } } + preference { + title = "Discord" + val url = "https://discord.gg/WrBkRk4" + summary = url + onClick { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + } + } preference { titleRes = R.string.version summary = if (BuildConfig.DEBUG) 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 1ac99c63c..e3c49d92c 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 @@ -32,8 +32,8 @@ class SettingsGeneralController : SettingsController() { listPreference { key = Keys.lang titleRes = R.string.pref_language - entryValues = arrayOf("", "bg", "en", "es", "fr", "it", "lv", "nl", "pt", "pt-BR", "ru", - "vi") + entryValues = arrayOf("", "ar", "bg", "de", "en", "es", "fr", "id", "it", "lv", "ms", + "nl", "pl", "pt", "pt-BR", "ru", "vi") entries = entryValues.map { value -> val locale = LocaleHelper.getLocaleFromString(value.toString()) locale?.getDisplayName(locale)?.capitalize() ?: 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 171d62629..82fa7e504 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 @@ -30,12 +30,6 @@ class SettingsMainController : SettingsController() { titleRes = R.string.pref_category_downloads onClick { navigateTo(SettingsDownloadController()) } } - preference { - iconRes = R.drawable.ic_language_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_sources - onClick { navigateTo(SettingsSourcesController()) } - } preference { iconRes = R.drawable.ic_sync_black_24dp iconTint = tintColor 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 c7982d0d8..25f847b72 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt @@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.setting import android.graphics.drawable.Drawable import android.support.v7.preference.PreferenceGroup import android.support.v7.preference.PreferenceScreen +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.SourceManager diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt index 7a705fe66..36252f610 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt @@ -20,7 +20,7 @@ import java.util.* fun syncChaptersWithSource(db: DatabaseHelper, rawSourceChapters: List, manga: Manga, - source: Source) : Pair, List> { + source: Source): Pair, List> { if (rawSourceChapters.isEmpty()) { throw Exception("No chapters found") @@ -38,7 +38,26 @@ fun syncChaptersWithSource(db: DatabaseHelper, } // Chapters from the source not in db. - val toAdd = sourceChapters.filterNot { it in dbChapters } + val toAdd = mutableListOf() + + // Chapters whose metadata have changed. + val toChange = mutableListOf() + + for (sourceChapter in sourceChapters) { + val dbChapter = dbChapters.find { it.url == sourceChapter.url } + + // Add the chapter if not in db already, or update if the metadata changed. + if (dbChapter == null) { + toAdd.add(sourceChapter) + } else if (dbChapter.scanlator != sourceChapter.scanlator || + dbChapter.name != sourceChapter.name) { + + dbChapter.scanlator = sourceChapter.scanlator + dbChapter.name = sourceChapter.name + + toChange.add(dbChapter) + } + } // Recognize number for new chapters. toAdd.forEach { @@ -49,10 +68,14 @@ fun syncChaptersWithSource(db: DatabaseHelper, } // Chapters from the db not in the source. - val toDelete = dbChapters.filterNot { it in sourceChapters } + val toDelete = dbChapters.filterNot { dbChapter -> + sourceChapters.any { sourceChapter -> + dbChapter.url == sourceChapter.url + } + } - // Return if there's nothing to add or delete, avoiding unnecessary db transactions. - if (toAdd.isEmpty() && toDelete.isEmpty()) { + // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. + if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { return Pair(emptyList(), emptyList()) } @@ -90,8 +113,13 @@ fun syncChaptersWithSource(db: DatabaseHelper, db.insertChapters(toAdd).executeAsBlocking() } + if (!toChange.isEmpty()) { + db.insertChapters(toChange).executeAsBlocking() + } + // Fix order in source. db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking() } return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList()) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index e37667f76..b777c87b3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -40,11 +40,12 @@ fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT) { /** * Helper method to create a notification. * + * @param id the channel id. * @param func the function that will execute inside the builder. * @return a notification to be displayed or updated. */ -inline fun Context.notification(func: NotificationCompat.Builder.() -> Unit): Notification { - val builder = NotificationCompat.Builder(this) +inline fun Context.notification(channelId: String, func: NotificationCompat.Builder.() -> Unit): Notification { + val builder = NotificationCompat.Builder(this, channelId) builder.func() return builder.build() } @@ -105,7 +106,7 @@ val Context.powerManager: PowerManager * * @param intent intent that contains broadcast information */ -fun Context.sendLocalBroadcast(intent:Intent){ +fun Context.sendLocalBroadcast(intent: Intent) { LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } @@ -142,6 +143,7 @@ fun Context.unregisterLocalReceiver(receiver: BroadcastReceiver) { fun Context.isServiceRunning(serviceClass: Class<*>): Boolean { val className = serviceClass.name val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + @Suppress("DEPRECATION") return manager.getRunningServices(Integer.MAX_VALUE) .any { className == it.service.className } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt new file mode 100644 index 000000000..8959794a1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.util + +import kotlinx.coroutines.experimental.CoroutineScope +import kotlinx.coroutines.experimental.CoroutineStart +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.launch + +fun launchUI(block: suspend CoroutineScope.() -> Unit): Job = + launch(UI, CoroutineStart.DEFAULT, block) + +fun launchNow(block: suspend CoroutineScope.() -> Unit): Job = + launch(UI, CoroutineStart.UNDISPATCHED, block) \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/LocaleHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/LocaleHelper.kt index df9c038ea..43e5415a3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/LocaleHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/LocaleHelper.kt @@ -89,6 +89,8 @@ object LocaleHelper { val newConfig = updateConfigLocale(config, currentLocale!!) val resources = app.resources resources.updateConfiguration(newConfig, resources.displayMetrics) + + Locale.setDefault(currentLocale) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt index 912f05e80..e3f6bbb01 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt @@ -4,9 +4,12 @@ package eu.kanade.tachiyomi.util import android.graphics.Color import android.graphics.Point +import android.graphics.Typeface import android.support.design.widget.Snackbar import android.view.View import android.widget.TextView +import com.amulyakhare.textdrawable.TextDrawable +import com.amulyakhare.textdrawable.util.ColorGenerator /** * Returns coordinates of view. @@ -25,7 +28,7 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2) */ inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit): Snackbar { val snack = Snackbar.make(this, message, length) - val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView + val textView: TextView = snack.view.findViewById(android.support.design.R.id.snackbar_text) textView.setTextColor(Color.WHITE) snack.f() snack.show() @@ -43,3 +46,21 @@ inline fun View.invisible() { inline fun View.gone() { visibility = View.GONE } + +/** + * Returns a TextDrawable determined by input + * + * @param text text of [TextDrawable] + * @param random random color + */ +fun View.getRound(text: String, random : Boolean = true): TextDrawable { + val size = Math.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)) +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationBase.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationBase.kt index 7a847f187..eaa4201e1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationBase.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationBase.kt @@ -10,15 +10,16 @@ abstract class FABAnimationBase : FloatingActionButton.Behavior() { var isAnimatingOut = false override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, - directTargetChild: View, target: View, nestedScrollAxes: Int): Boolean { + directTargetChild: View, target: View, axes: Int, type: Int): Boolean { // Ensure we react to vertical scrolling - return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || - super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes) + return axes == ViewCompat.SCROLL_AXIS_VERTICAL || + super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type) } - override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, target: View, - dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) { - super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) + override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, + target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, + dyUnconsumed: Int, type: Int) { + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type) if (dyConsumed > 0 && !isAnimatingOut && child.visibility == View.VISIBLE) { // User scrolled down and the FAB is currently visible -> hide the FAB animateOut(child) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationUpDown.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationUpDown.kt index ea9e0f89e..e1c4245d2 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationUpDown.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/FABAnimationUpDown.kt @@ -9,6 +9,7 @@ import android.view.animation.Animation import android.view.animation.AnimationUtils import eu.kanade.tachiyomi.R +@Suppress("unused", "UNUSED_PARAMETER") class FABAnimationUpDown @JvmOverloads constructor(ctx: Context, attrs: AttributeSet? = null) : FABAnimationBase() { private val INTERPOLATOR = FastOutSlowInInterpolator() 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 f813f799c..4152f1767 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/PTSansTextView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/PTSansTextView.kt @@ -38,10 +38,10 @@ class PTSansTextView @JvmOverloads constructor(context: Context, attrs: Attribut } } - override fun draw(canvas: Canvas) { + override fun onDraw(canvas: Canvas) { // Draw two times for a more visible shadow around the text - super.draw(canvas) - super.draw(canvas) + super.onDraw(canvas) + super.onDraw(canvas) } } 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 40b1cfa01..efae6f62f 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt @@ -48,6 +48,7 @@ open class SimpleNavigationView @JvmOverloads constructor( R.styleable.NavigationView_elevation, 0).toFloat()) } + @Suppress("DEPRECATION") ViewCompat.setFitsSystemWindows(this, a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false)) @@ -61,6 +62,7 @@ open class SimpleNavigationView @JvmOverloads constructor( /** * Overriden to measure the width of the navigation view. */ + @SuppressLint("SwitchIntDef") override fun onMeasure(widthSpec: Int, heightSpec: Int) { val width = when (MeasureSpec.getMode(widthSpec)) { MeasureSpec.AT_MOST -> MeasureSpec.makeMeasureSpec( @@ -104,7 +106,7 @@ open class SimpleNavigationView @JvmOverloads constructor( class RadioHolder(parent: ViewGroup, listener: View.OnClickListener?) : ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) { - val radio = itemView.findViewById(TR.id.nav_view_item) as RadioButton + val radio: RadioButton = itemView.findViewById(TR.id.nav_view_item) } /** @@ -113,7 +115,7 @@ open class SimpleNavigationView @JvmOverloads constructor( class CheckboxHolder(parent: ViewGroup, listener: View.OnClickListener?) : ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) { - val check = itemView.findViewById(TR.id.nav_view_item) as CheckBox + val check: CheckBox = itemView.findViewById(TR.id.nav_view_item) } /** @@ -122,21 +124,21 @@ open class SimpleNavigationView @JvmOverloads constructor( class MultiStateHolder(parent: ViewGroup, listener: View.OnClickListener?) : ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) { - val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView + 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) { - val text = itemView.findViewById(TR.id.nav_view_item_text) as TextView - val spinner = itemView.findViewById(TR.id.nav_view_item) as Spinner + 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)) { - val wrapper = itemView.findViewById(TR.id.nav_view_item_wrapper) as TextInputLayout - val edit = itemView.findViewById(TR.id.nav_view_item) as EditText + val wrapper: TextInputLayout = itemView.findViewById(TR.id.nav_view_item_wrapper) + val edit: EditText = itemView.findViewById(TR.id.nav_view_item) } protected companion object { 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 af12756dd..75d6e74c8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt @@ -5,9 +5,8 @@ import android.support.graphics.drawable.VectorDrawableCompat import android.view.View import android.widget.ImageView import android.widget.ImageView.ScaleType -import com.bumptech.glide.load.resource.drawable.GlideDrawable -import com.bumptech.glide.request.animation.GlideAnimation -import com.bumptech.glide.request.target.GlideDrawableImageViewTarget +import com.bumptech.glide.request.target.ImageViewTarget +import com.bumptech.glide.request.transition.Transition import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.gone @@ -26,16 +25,23 @@ class StateImageViewTarget(view: ImageView, val progress: View? = null, val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp, val errorScaleType: ScaleType = ScaleType.CENTER) : - GlideDrawableImageViewTarget(view) { + + ImageViewTarget(view) { + + private var resource: Drawable? = null private val imageScaleType = view.scaleType + override fun setResource(resource: Drawable?) { + view.setImageDrawable(resource) + } + override fun onLoadStarted(placeholder: Drawable?) { progress?.visible() super.onLoadStarted(placeholder) } - override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) { + override fun onLoadFailed(errorDrawable: Drawable?) { progress?.gone() view.scaleType = errorScaleType @@ -49,9 +55,10 @@ class StateImageViewTarget(view: ImageView, super.onLoadCleared(placeholder) } - override fun onResourceReady(resource: GlideDrawable?, animation: GlideAnimation?) { + override fun onResourceReady(resource: Drawable?, transition: Transition?) { progress?.gone() view.scaleType = imageScaleType - super.onResourceReady(resource, animation) + super.onResourceReady(resource, transition) + this.resource = resource } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java b/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java deleted file mode 100644 index 54cb768c3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2016 Davide Steduto - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package eu.kanade.tachiyomi.widget; - -import android.content.Context; -import android.graphics.Color; -import android.support.annotation.ColorInt; -import android.support.annotation.IntDef; -import android.support.annotation.IntRange; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; -import android.support.design.widget.Snackbar; -import android.view.View; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; - -import eu.davidea.flexibleadapter.FlexibleAdapter; - -/** - * Helper to simplify the Undo operation with FlexibleAdapter. - * - * @author Davide Steduto - * @since 30/04/2016 - */ -@SuppressWarnings("WeakerAccess") -public class UndoHelper extends Snackbar.Callback { - - /** - * Default undo-timeout of 5''. - */ - public static final int UNDO_TIMEOUT = 5000; - /** - * Indicates that the Confirmation Listener (Undo and Delete) will perform a deletion. - */ - public static final int ACTION_REMOVE = 0; - /** - * Indicates that the Confirmation Listener (Undo and Delete) will perform an update. - */ - public static final int ACTION_UPDATE = 1; - - /** - * Annotation interface for Undo actions. - */ - @IntDef({ACTION_REMOVE, ACTION_UPDATE}) - @Retention(RetentionPolicy.SOURCE) - public @interface Action { - } - - @Action - private int mAction = ACTION_REMOVE; - private List mPositions = null; - private Object mPayload = null; - private FlexibleAdapter mAdapter; - private Snackbar mSnackbar = null; - private OnActionListener mActionListener; - private OnUndoListener mUndoListener; - private @ColorInt int mActionTextColor = Color.TRANSPARENT; - - - /** - * Default constructor. - *

By calling this constructor, {@link FlexibleAdapter#setPermanentDelete(boolean)} - * is set {@code false} automatically. - * - * @param adapter the instance of {@code FlexibleAdapter} - * @param undoListener the callback for the Undo and Delete confirmation - */ - public UndoHelper(FlexibleAdapter adapter, OnUndoListener undoListener) { - this.mAdapter = adapter; - this.mUndoListener = undoListener; - adapter.setPermanentDelete(false); - } - - /** - * Sets the payload to inform other linked items about the change in action. - * - * @param payload any non-null user object to notify the parent (the payload will be - * therefore passed to the bind method of the parent ViewHolder), - * pass null to not notify the parent - * @return this object, so it can be chained - */ - public UndoHelper withPayload(Object payload) { - this.mPayload = payload; - return this; - } - - /** - * By default {@link UndoHelper#ACTION_REMOVE} is performed. - * - * @param action the action, one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} - * @param actionListener the listener for the custom action to perform before the deletion - * @return this object, so it can be chained - */ - public UndoHelper withAction(@Action int action, @NonNull OnActionListener actionListener) { - this.mAction = action; - this.mActionListener = actionListener; - return this; - } - - /** - * Sets the text color of the action. - * - * @param color the color for the action button - * @return this object, so it can be chained - */ - public UndoHelper withActionTextColor(@ColorInt int color) { - this.mActionTextColor = color; - return this; - } - - /** - * As {@link #remove(List, View, CharSequence, CharSequence, int)} but with String - * resources instead of CharSequence. - */ - public void remove(List positions, @NonNull View mainView, - @StringRes int messageStringResId, @StringRes int actionStringResId, - @IntRange(from = -1) int undoTime) { - Context context = mainView.getContext(); - remove(positions, mainView, context.getString(messageStringResId), - context.getString(actionStringResId), undoTime); - } - - /** - * Performs the action on the specified positions and displays a SnackBar to Undo - * the operation. To customize the UPDATE event, please set a custom listener with - * {@link #withAction(int, OnActionListener)} method. - *

By default the DELETE action will be performed.

- * - * @param positions the position to delete or update - * @param mainView the view to find a parent from - * @param message the text to show. Can be formatted text - * @param actionText the action text to display - * @param undoTime How long to display the message. Either {@link Snackbar#LENGTH_SHORT} or - * {@link Snackbar#LENGTH_LONG} or any custom Integer. - * @see #remove(List, View, int, int, int) - */ - @SuppressWarnings("WrongConstant") - public void remove(List positions, @NonNull View mainView, - CharSequence message, CharSequence actionText, - @IntRange(from = -1) int undoTime) { - this.mPositions = positions; - Snackbar snackbar; - if (!mAdapter.isPermanentDelete()) { - snackbar = Snackbar.make(mainView, message, undoTime > 0 ? undoTime + 400 : undoTime) - .setAction(actionText, new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mUndoListener != null) - mUndoListener.onUndoConfirmed(mAction); - } - }); - } else { - snackbar = Snackbar.make(mainView, message, undoTime); - } - if (mActionTextColor != Color.TRANSPARENT) { - snackbar.setActionTextColor(mActionTextColor); - } - mSnackbar = snackbar; - snackbar.addCallback(this); - snackbar.show(); - } - - public void dismissNow() { - if (mSnackbar != null) { - mSnackbar.removeCallback(this); - mSnackbar.dismiss(); - onDismissed(mSnackbar, Snackbar.Callback.DISMISS_EVENT_MANUAL); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onDismissed(Snackbar snackbar, int event) { - if (mAdapter.isPermanentDelete()) return; - switch (event) { - case DISMISS_EVENT_SWIPE: - case DISMISS_EVENT_MANUAL: - case DISMISS_EVENT_TIMEOUT: - if (mUndoListener != null) - mUndoListener.onDeleteConfirmed(mAction); - mAdapter.emptyBin(); - mSnackbar = null; - case DISMISS_EVENT_CONSECUTIVE: - case DISMISS_EVENT_ACTION: - default: - break; - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onShown(Snackbar snackbar) { - boolean consumed = false; - // Perform the action before deletion - if (mActionListener != null) consumed = mActionListener.onPreAction(); - // Remove selected items from Adapter list after SnackBar is shown - if (!consumed) mAdapter.removeItems(mPositions, mPayload); - // Perform the action after the deletion - if (mActionListener != null) mActionListener.onPostAction(); - // Here, we can notify the callback only in case of permanent deletion - if (mAdapter.isPermanentDelete() && mUndoListener != null) - mUndoListener.onDeleteConfirmed(mAction); - } - - /** - * Basic implementation of {@link OnActionListener} interface. - *

Override the methods as your convenience.

- */ - public static class SimpleActionListener implements OnActionListener { - @Override - public boolean onPreAction() { - return false; - } - - @Override - public void onPostAction() { - - } - } - - public interface OnActionListener { - /** - * Performs the custom action before item deletion. - * - * @return true if action has been consumed and should stop the deletion, false to - * continue with the deletion - */ - boolean onPreAction(); - - /** - * Performs custom action After items deletion. Useful to finish the action mode and perform - * secondary custom actions. - */ - void onPostAction(); - } - - /** - * @since 30/04/2016 - */ - public interface OnUndoListener { - /** - * Called when Undo event is triggered. Perform custom action after restoration. - *

Usually for a delete restoration you should call - * {@link FlexibleAdapter#restoreDeletedItems()}.

- * - * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} - */ - void onUndoConfirmed(int action); - - /** - * Called when Undo timeout is over and action must be committed in the user Database. - *

Due to Java Generic, it's too complicated and not well manageable if we pass the - * List<T> object.
- * So, to get deleted items, use {@link FlexibleAdapter#getDeletedItems()} from the - * implementation of this method.

- * - * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} - */ - void onDeleteConfirmed(int action); - } - -} \ No newline at end of file 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 1b355d490..b27c77c62 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -31,14 +31,14 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController( .negativeText(android.R.string.cancel) .build() - onViewCreated(dialog.view, savedState) + onViewCreated(dialog.view) return dialog } - fun onViewCreated(view: View, savedState: Bundle?) { + fun onViewCreated(view: View) { v = view.apply { - show_password.setOnCheckedChangeListener { v, isChecked -> + show_password.setOnCheckedChangeListener { _, isChecked -> if (isChecked) password.transformationMethod = null else diff --git a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml index 4ad2729af..b21f488c2 100644 --- a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml +++ b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml @@ -1,7 +1,7 @@ @@ -18,6 +18,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_dark.xml b/app/src/main/res/drawable-v21/library_item_selector_dark.xml index e78c6ec16..82a72da4a 100755 --- a/app/src/main/res/drawable-v21/library_item_selector_dark.xml +++ b/app/src/main/res/drawable-v21/library_item_selector_dark.xml @@ -18,6 +18,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_light.xml b/app/src/main/res/drawable-v21/library_item_selector_light.xml index c85ee3913..1f2e8bf89 100755 --- a/app/src/main/res/drawable-v21/library_item_selector_light.xml +++ b/app/src/main/res/drawable-v21/library_item_selector_light.xml @@ -18,6 +18,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml index 96494d93a..0fce81a34 100644 --- a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml +++ b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml @@ -1,6 +1,6 @@ + android:color="@color/rippleColorDark"> diff --git a/app/src/main/res/drawable-v21/list_item_selector_dark.xml b/app/src/main/res/drawable-v21/list_item_selector_dark.xml index 07ed74dd4..07b9ef6d5 100755 --- a/app/src/main/res/drawable-v21/list_item_selector_dark.xml +++ b/app/src/main/res/drawable-v21/list_item_selector_dark.xml @@ -1,6 +1,6 @@ + android:color="@color/rippleColorDark"> diff --git a/app/src/main/res/drawable-v21/list_item_selector_light.xml b/app/src/main/res/drawable-v21/list_item_selector_light.xml index 692d94f2b..942446ef0 100755 --- a/app/src/main/res/drawable-v21/list_item_selector_light.xml +++ b/app/src/main/res/drawable-v21/list_item_selector_light.xml @@ -1,6 +1,6 @@ + android:color="@color/rippleColorLight"> diff --git a/app/src/main/res/drawable/ic_search_black_112dp.xml b/app/src/main/res/drawable/ic_search_black_112dp.xml new file mode 100644 index 000000000..05705a607 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_black_112dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/library_item_selector_amoled.xml b/app/src/main/res/drawable/library_item_selector_amoled.xml index 92cb0db94..1cf05bdc9 100644 --- a/app/src/main/res/drawable/library_item_selector_amoled.xml +++ b/app/src/main/res/drawable/library_item_selector_amoled.xml @@ -1,10 +1,10 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector_dark.xml b/app/src/main/res/drawable/library_item_selector_dark.xml index 73de4df07..9880c4b38 100755 --- a/app/src/main/res/drawable/library_item_selector_dark.xml +++ b/app/src/main/res/drawable/library_item_selector_dark.xml @@ -1,10 +1,10 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector_light.xml b/app/src/main/res/drawable/library_item_selector_light.xml index 9273e00fe..70f7b85b4 100755 --- a/app/src/main/res/drawable/library_item_selector_light.xml +++ b/app/src/main/res/drawable/library_item_selector_light.xml @@ -1,19 +1,10 @@ - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_item_selector_amoled.xml b/app/src/main/res/drawable/list_item_selector_amoled.xml index e573c82bb..9bbf56578 100644 --- a/app/src/main/res/drawable/list_item_selector_amoled.xml +++ b/app/src/main/res/drawable/list_item_selector_amoled.xml @@ -1,6 +1,6 @@ + android:exitFadeDuration="@android:integer/config_longAnimTime"> 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 dd0779885..60034f818 100755 --- a/app/src/main/res/drawable/list_item_selector_dark.xml +++ b/app/src/main/res/drawable/list_item_selector_dark.xml @@ -1,6 +1,6 @@ + android:exitFadeDuration="@android:integer/config_longAnimTime"> diff --git a/app/src/main/res/drawable/list_item_selector_light.xml b/app/src/main/res/drawable/list_item_selector_light.xml index 73e54c92f..92bed9fc9 100755 --- a/app/src/main/res/drawable/list_item_selector_light.xml +++ b/app/src/main/res/drawable/list_item_selector_light.xml @@ -1,15 +1,6 @@ - - - - - - - - - + android:exitFadeDuration="@android:integer/config_longAnimTime"> diff --git a/app/src/main/res/drawable/text_button.xml b/app/src/main/res/drawable/text_button.xml new file mode 100644 index 000000000..ef5c24c56 --- /dev/null +++ b/app/src/main/res/drawable/text_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ 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 48d8601ba..17ba20e10 100755 --- a/app/src/main/res/layout/catalogue_controller.xml +++ b/app/src/main/res/layout/catalogue_controller.xml @@ -6,12 +6,12 @@ android:layout_height="match_parent"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" + android:orientation="vertical" + android:id="@+id/catalogue_view" + tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController">