diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 755c6ba5d..0c2dda36b 100755 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,5 +31,4 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75 # Translations -File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated. -Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs) +[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation) diff --git a/app/build.gradle b/app/build.gradle index 4368b0753..e4d1cd81c 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,20 +96,17 @@ android { checkReleaseBuilds false } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - } 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' // Android support library - final support_library_version = '25.3.1' + 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" @@ -124,23 +121,23 @@ dependencies { // ReactiveX compile 'io.reactivex:rxandroid:1.2.1' - compile 'io.reactivex:rxjava:1.2.9' + 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' // Network client - compile "com.squareup.okhttp3:okhttp:3.6.0" - compile 'com.squareup.okio:okio:1.11.0' + compile "com.squareup.okhttp3:okhttp:3.8.1" + compile 'com.squareup.okio:okio:1.13.0' // REST - final retrofit_version = '2.2.0' + 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" // JSON - compile 'com.google.code.gson:gson:2.8.0' + compile 'com.google.code.gson:gson:2.8.1' compile 'com.github.salomonbrys.kotson:kotson:2.5.0' // YAML @@ -157,27 +154,26 @@ dependencies { compile 'org.jsoup:jsoup:1.10.2' // Job scheduling - compile 'com.evernote:android-job:1.1.8' - compile 'com.google.android.gms:play-services-gcm:10.2.0' + compile 'com.evernote:android-job:1.1.11' + compile 'com.google.android.gms:play-services-gcm:11.0.1' // Changelog compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' // Database - compile "com.pushtorefresh.storio:sqlite:1.12.3" + compile "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-v4:$nucleus_version" compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version" // Dependency injection compile "uy.kohesive.injekt:injekt-core:1.16.1" // Image library - compile 'com.github.bumptech.glide:glide:3.7.0' - compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar' + compile 'com.github.bumptech.glide:glide:3.8.0' + compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar' // Transformations compile 'jp.wasabeef:glide-transformations:2.0.2' @@ -194,13 +190,22 @@ dependencies { 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.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed compile 'com.nononsenseapps:filepicker:2.5.2' compile 'com.github.amulyakhare:TextDrawable:558677e' - compile 'com.afollestad.material-dialogs:core:0.9.4.2' - compile 'net.xpece.android:support-preference:1.2.5' + compile 'com.afollestad.material-dialogs:core:0.9.4.5' compile 'me.zhanghai.android.systemuihelper:library:1.0.0' - compile 'de.hdodenhof:circleimageview:2.1.0' + compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' + + // Conductor + compile "com.bluelinelabs:conductor:2.1.4" + compile 'com.github.inorichi:conductor-support-preference:9e36460' + + // 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" //Firebase (EH) final firebase_version = '10.0.1' @@ -232,7 +237,7 @@ dependencies { } buildscript { - ext.kotlin_version = '1.1.1' + ext.kotlin_version = '1.1.3' repositories { mavenCentral() } @@ -251,7 +256,7 @@ configurations.all { def requested = details.requested if (requested.group == 'com.android.support') { if (!requested.name.startsWith("multidex")) { - details.useVersion '25.3.1' + details.useVersion '25.4.0' } } } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ffe90a1ee..4bab22771 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,24 +1,21 @@ -dontobfuscate +-dontwarn eu.kanade.tachiyomi.** -keep class eu.kanade.tachiyomi.** -keep class eu.kanade.tachiyomi.source.model.** { *; } -keep class com.hippo.image.** { *; } -keep interface com.hippo.image.** { *; } +# Extensions may require methods unused in the core app +-keep class org.jsoup.** { *; } +-keep class kotlin.** { *; } + # OkHttp --keepattributes Signature --keepattributes *Annotation* --keep class okhttp3.** { *; } --keep interface okhttp3.** { *; } -dontwarn okhttp3.** -dontwarn okio.** - -# Okio --keep class sun.misc.Unsafe { *; } --dontwarn java.nio.file.* --dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement --dontwarn okio.** +-dontwarn javax.annotation.** +-dontwarn retrofit2.Platform$Java8 # Glide specific rules # # https://github.com/bumptech/glide @@ -44,27 +41,26 @@ rx.internal.util.atomic.LinkedQueueNode consumerNode; } -# Retrofit 2.X -## https://square.github.io/retrofit/ ## +### Support v7, Design +# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051 +-keep class android.support.v7.widget.RoundRectDrawable { *; } --dontwarn retrofit2.** --keep class retrofit2.** { *; } --keepattributes Signature --keepattributes Exceptions - --keepclasseswithmembers class * { - @retrofit2.http.* ; -} - -# AppCombat -keep public class android.support.v7.widget.** { *; } -keep public class android.support.v7.internal.widget.** { *; } -keep public class android.support.v7.internal.view.menu.** { *; } +-keep public class android.support.v7.graphics.drawable.** { *; } -keep public class * extends android.support.v4.view.ActionProvider { public (android.content.Context); } +-dontwarn android.support.** +-dontwarn android.support.design.** +-keep class android.support.design.** { *; } +-keep interface android.support.design.** { *; } +-keep public class android.support.design.R$* { *; } + + # ReactiveNetwork -dontwarn com.github.pwittchen.reactivenetwork.** @@ -74,15 +70,8 @@ # removes such information by default, so configure it to keep all of it. -keepattributes Signature -# For using GSON @Expose annotation --keepattributes *Annotation* - # Gson specific classes -keep class sun.misc.Unsafe { *; } -#-keep class com.google.gson.stream.** { *; } - -# Application classes that will be serialized/deserialized over Gson --keep class com.google.gson.examples.android.model.** { *; } # Prevent proguard from stripping interface information from TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) @@ -92,7 +81,6 @@ # SnakeYaml -keep class org.yaml.snakeyaml.** { public protected private *; } --keep class org.yaml.snakeyaml.** { public protected private *; } -dontwarn org.yaml.snakeyaml.** # Duktape diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fef49b7ab..0a886481f 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ @@ -9,9 +8,6 @@ - - + @@ -35,21 +33,9 @@ - - - - (BackupConst.EXTRA_URI) val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false) val flags = intent.getIntExtra(EXTRA_FLAGS, 0) // Create backup - createBackupFromApp(Uri.parse(uri), flags, isJob) + createBackupFromApp(uri, flags, isJob) } /** @@ -150,9 +143,9 @@ class BackupCreateService : IntentService(NAME) { } // Show completed dialog - val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply { - putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_BACKUP_COMPLETED_DIALOG) - putExtra(SettingsBackupFragment.EXTRA_URI, file.uri.toString()) + val intent = Intent(BackupConst.INTENT_FILTER).apply { + putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG) + putExtra(BackupConst.EXTRA_URI, file.uri.toString()) } sendLocalBroadcast(intent) } @@ -160,9 +153,9 @@ class BackupCreateService : IntentService(NAME) { Timber.e(e) if (!isJob) { // Show error dialog - val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply { - putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_BACKUP_DIALOG) - putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, e.message) + val intent = Intent(BackupConst.INTENT_FILTER).apply { + putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG) + putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message) } sendLocalBroadcast(intent) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt index 6f4796be6..b23157c50 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.backup +import android.net.Uri import com.evernote.android.job.Job import com.evernote.android.job.JobManager import com.evernote.android.job.JobRequest @@ -7,14 +8,15 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.io.File class BackupCreatorJob : Job() { override fun onRunJob(params: Params): Result { val preferences = Injekt.get() - val path = preferences.backupsDirectory().getOrDefault() + val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault())) val flags = BackupCreateService.BACKUP_ALL - BackupCreateService.makeBackup(context,path,flags,true) + BackupCreateService.makeBackup(context, uri, flags, true) return Result.SUCCESS } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index 2fc1998d5..812434abe 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.util.syncChaptersWithSource import rx.Observable import uy.kohesive.injekt.injectLazy -import java.util.* class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { 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 02df6b303..4179e0f0a 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 @@ -22,9 +22,8 @@ import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment -import eu.kanade.tachiyomi.util.AndroidComponentUtil import eu.kanade.tachiyomi.util.chop +import eu.kanade.tachiyomi.util.isServiceRunning import eu.kanade.tachiyomi.util.sendLocalBroadcast import rx.Observable import rx.Subscription @@ -36,7 +35,6 @@ import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID /** * Restores backup from json file @@ -44,11 +42,6 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID class BackupRestoreService : Service() { companion object { - // Name of service - private const val NAME = "BackupRestoreService" - - // Uri as string - private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI" /** * Returns the status of the service. @@ -57,7 +50,7 @@ class BackupRestoreService : Service() { * @return true if the service is running, false otherwise. */ fun isRunning(context: Context): Boolean { - return AndroidComponentUtil.isServiceRunning(context, BackupRestoreService::class.java) + return context.isServiceRunning(BackupRestoreService::class.java) } /** @@ -69,7 +62,7 @@ class BackupRestoreService : Service() { fun start(context: Context, uri: Uri) { if (!isRunning(context)) { val intent = Intent(context, BackupRestoreService::class.java).apply { - putExtra(EXTRA_URI, uri) + putExtra(BackupConst.EXTRA_URI, uri) } context.startService(intent) } @@ -164,7 +157,7 @@ class BackupRestoreService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) return Service.START_NOT_STICKY - val uri = intent.getParcelableExtra(EXTRA_URI) + val uri = intent.getParcelableExtra(BackupConst.EXTRA_URI) // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() @@ -236,12 +229,12 @@ class BackupRestoreService : Service() { val endTime = System.currentTimeMillis() val time = endTime - startTime val logFile = writeErrorLog() - val completeIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply { - putExtra(SettingsBackupFragment.EXTRA_TIME, time) - putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors.size) - putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE_PATH, logFile.parent) - putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE, logFile.name) - putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_RESTORE_COMPLETED_DIALOG) + val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { + putExtra(BackupConst.EXTRA_TIME, time) + putExtra(BackupConst.EXTRA_ERRORS, errors.size) + putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) + putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) + putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG) } sendLocalBroadcast(completeIntent) @@ -249,9 +242,9 @@ class BackupRestoreService : Service() { .doOnError { error -> Timber.e(error) writeErrorLog() - val errorIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply { - putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_RESTORE_DIALOG) - putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, error.message) + val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { + putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG) + putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) } sendLocalBroadcast(errorIntent) } @@ -392,7 +385,7 @@ class BackupRestoreService : Service() { /** - * Called to update dialog in [SettingsBackupFragment] + * Called to update dialog in [BackupConst] * * @param progress restore progress * @param amount total restoreAmount of manga @@ -400,12 +393,12 @@ class BackupRestoreService : Service() { */ private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int, content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) { - val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply { - putExtra(SettingsBackupFragment.EXTRA_PROGRESS, progress) - putExtra(SettingsBackupFragment.EXTRA_AMOUNT, amount) - putExtra(SettingsBackupFragment.EXTRA_CONTENT, content) - putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors) - putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_SET_PROGRESS_DIALOG) + val intent = Intent(BackupConst.INTENT_FILTER).apply { + putExtra(BackupConst.EXTRA_PROGRESS, progress) + putExtra(BackupConst.EXTRA_AMOUNT, amount) + putExtra(BackupConst.EXTRA_CONTENT, content) + putExtra(BackupConst.EXTRA_ERRORS, errors) + putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG) } sendLocalBroadcast(intent) } 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 6f75d0db6..0b970356f 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,9 +44,13 @@ 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(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY), + private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY), PARAMETER_APP_VERSION, PARAMETER_VALUE_COUNT, PARAMETER_CACHE_SIZE) @@ -187,12 +191,12 @@ class ChapterCache(private val context: Context) { editor = diskCache.edit(key) ?: throw IOException("Unable to edit key") // Get OutputStream and write image with Okio. - response.body().source().saveTo(editor.newOutputStream(0)) + response.body()!!.source().saveTo(editor.newOutputStream(0)) diskCache.flush() editor.commit() } finally { - response.body().close() + response.body()?.close() editor?.abortUnlessCommitted() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt index 40e5d129a..f8fd268cf 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt @@ -17,7 +17,7 @@ class DbOpenHelper(context: Context) /** * Version of the database. */ - const val DATABASE_VERSION = 4 + const val DATABASE_VERSION = 5 } override fun onCreate(db: SQLiteDatabase) = with(db) { @@ -51,6 +51,9 @@ class DbOpenHelper(context: Context) if (oldVersion < 4) { db.execSQL(ChapterTable.bookmarkUpdateQuery) } + if (oldVersion < 5) { + db.execSQL(ChapterTable.addScanlator) + } } override fun onConfigure(db: SQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt index 0065f5f44..2e903a64b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt @@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ +import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE @@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver() { put(COL_URL, obj.url) put(COL_NAME, obj.name) put(COL_READ, obj.read) + put(COL_SCANLATOR, obj.scanlator) put(COL_BOOKMARK, obj.bookmark) put(COL_DATE_FETCH, obj.date_fetch) put(COL_DATE_UPLOAD, obj.date_upload) @@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver() { manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) url = cursor.getString(cursor.getColumnIndex(COL_URL)) name = cursor.getString(cursor.getColumnIndex(COL_NAME)) + scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR)) read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1 bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1 date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH)) 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 f7f1d0662..a7fd291b5 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 @@ -10,6 +10,8 @@ class ChapterImpl : Chapter { override lateinit var name: String + override var scanlator: String? = null + override var read: Boolean = false override var bookmark: Boolean = false @@ -29,8 +31,9 @@ class ChapterImpl : Chapter { if (other == null || javaClass != other.javaClass) return false val chapter = other as Chapter - - return url == chapter.url + // 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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index 7fed020fc..e11fe8f83 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models * @param chapter object containing chater * @param history object containing history */ -class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) +data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) 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 33eb06558..ed5ce5e8b 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 @@ -98,4 +98,7 @@ interface MangaQueries : DbProvider { .observesTables(MangaTable.TABLE) .build()) .prepare() + + fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java) + .withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare(); } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index cac21db3f..daa5c48fd 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -93,6 +93,15 @@ fun getLastReadMangaQuery() = """ ORDER BY max DESC """ +fun getTotalChapterMangaQuery()= """ + SELECT ${Manga.TABLE}.* + FROM ${Manga.TABLE} + JOIN ${Chapter.TABLE} + ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} + GROUP BY ${Manga.TABLE}.${Manga.COL_ID} + ORDER by COUNT(*) +""" + /** * Query to get the categories for a manga. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt index e28f848cc..a031c94d4 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt @@ -14,6 +14,8 @@ object ChapterTable { const val COL_READ = "read" + const val COL_SCANLATOR = "scanlator" + const val COL_BOOKMARK = "bookmark" const val COL_DATE_FETCH = "date_fetch" @@ -32,6 +34,7 @@ object ChapterTable { $COL_MANGA_ID INTEGER NOT NULL, $COL_URL TEXT NOT NULL, $COL_NAME TEXT NOT NULL, + $COL_SCANLATOR TEXT, $COL_READ BOOLEAN NOT NULL, $COL_BOOKMARK BOOLEAN NOT NULL, $COL_LAST_PAGE_READ INT NOT NULL, @@ -52,4 +55,7 @@ object ChapterTable { val bookmarkUpdateQuery: String get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE" + val addScanlator: String + get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL" + } 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 82a3bcbec..f0aba1a18 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 @@ -114,6 +114,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro val pending = queue.filter { it.status != Download.DOWNLOADED } pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } + // Show download notification when simultaneous download > 1. + notifier.onProgressChange(queue) + downloadsRelay.call(pending) return !pending.isEmpty() } @@ -380,7 +383,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro .map { response -> val file = tmpDir.createFile("$filename.tmp") try { - response.body().source().saveTo(file.openOutputStream()) + response.body()!!.source().saveTo(file.openOutputStream()) val extension = getImageExtension(response, file) file.renameTo("$filename.$extension") } catch (e: Exception) { @@ -403,7 +406,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro */ private fun getImageExtension(response: Response, file: UniFile): String { // Read content type if available. - val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" } + val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" } // Else guess from the uri. ?: context.contentResolver.getType(file.uri) // Else read magic numbers. 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 e2eda3b72..4d30b38af 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 @@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource @@ -48,7 +49,8 @@ class LibraryUpdateService( val db: DatabaseHelper = Injekt.get(), val sourceManager: SourceManager = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(), - val downloadManager: DownloadManager = Injekt.get() + val downloadManager: DownloadManager = Injekt.get(), + val trackManager: TrackManager = Injekt.get() ) : Service() { /** @@ -85,17 +87,26 @@ class LibraryUpdateService( .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) } + /** + * Defines what should be updated within a service execution. + */ + enum class Target { + CHAPTERS, // Manga chapters + DETAILS, // Manga metadata + TRACKING // Tracking metadata + } + companion object { /** * Key for category to update. */ - const val UPDATE_CATEGORY = "category" + const val KEY_CATEGORY = "category" /** - * Key for updating the details instead of the chapters. + * Key that defines what should be updated. */ - const val UPDATE_DETAILS = "details" + const val KEY_TARGET = "target" /** * Returns the status of the service. @@ -104,7 +115,7 @@ class LibraryUpdateService( * @return true if the service is running, false otherwise. */ fun isRunning(context: Context): Boolean { - return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java) + return context.isServiceRunning(LibraryUpdateService::class.java) } /** @@ -113,13 +124,13 @@ class LibraryUpdateService( * * @param context the application context. * @param category a specific category to update, or null for global update. - * @param details whether to update the details instead of the list of chapters. + * @param target defines what should be updated. */ - fun start(context: Context, category: Category? = null, details: Boolean = false) { + fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) { if (!isRunning(context)) { val intent = Intent(context, LibraryUpdateService::class.java).apply { - putExtra(UPDATE_DETAILS, details) - category?.let { putExtra(UPDATE_CATEGORY, it.id) } + putExtra(KEY_TARGET, target) + category?.let { putExtra(KEY_CATEGORY, it.id) } } context.startService(intent) } @@ -176,6 +187,8 @@ class LibraryUpdateService( */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) return Service.START_NOT_STICKY + val target = intent.getSerializableExtra(KEY_TARGET) as? Target + ?: return Service.START_NOT_STICKY // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() @@ -183,13 +196,14 @@ class LibraryUpdateService( // Update favorite manga. Destroy service when completed or in case of an error. subscription = Observable .defer { - val mangaList = getMangaToUpdate(intent) + val mangaList = getMangaToUpdate(intent, target) // Update either chapter list or manga details. - if (!intent.getBooleanExtra(UPDATE_DETAILS, false)) - updateChapterList(mangaList) - else - updateDetails(mangaList) + when (target) { + Target.CHAPTERS -> updateChapterList(mangaList) + Target.DETAILS -> updateDetails(mangaList) + Target.TRACKING -> updateTrackings(mangaList) + } } .subscribeOn(Schedulers.io()) .subscribe({ @@ -207,10 +221,11 @@ class LibraryUpdateService( * Returns the list of manga to be updated. * * @param intent the update intent. + * @param target the target to update. * @return a list of manga to update */ - fun getMangaToUpdate(intent: Intent): List { - val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1) + fun getMangaToUpdate(intent: Intent, target: Target): List { + val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) var listToUpdate = if (categoryId != -1) db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } @@ -224,7 +239,7 @@ class LibraryUpdateService( db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } } - if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) { + if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } } @@ -328,8 +343,6 @@ class LibraryUpdateService( /** * Method that updates the details of the given list of manga. It's called in a background * thread, so it's safe to do heavy operations or network calls here. - * For each manga it calls [updateManga] and updates the notification showing the current - * progress. * * @param mangaToUpdate the list to update * @return an observable delivering the progress of each update. @@ -360,6 +373,42 @@ 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 { + // Initialize the variables holding the progress of the updates. + var count = 0 + + val loggedServices = trackManager.services.filter { it.isLogged } + + // Emit each manga and update it sequentially. + return Observable.from(mangaToUpdate) + // Notify manga that will update. + .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } + // Update the tracking details. + .concatMap { manga -> + val tracks = db.getTracks(manga).executeAsBlocking() + + Observable.from(tracks) + .concatMap { track -> + val service = trackManager.getService(track.sync_id) + if (service != null && service in loggedServices) { + service.refresh(track) + .doOnNext { db.insertTrack(it).executeAsBlocking() } + .onErrorReturn { track } + } else { + Observable.empty() + } + } + .map { manga } + } + .doOnCompleted { + cancelProgressNotification() + } + } + /** * Shows the notification containing the currently updating manga and the progress. * @@ -426,6 +475,7 @@ class LibraryUpdateService( private fun getNotificationIntent(): PendingIntent { val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt index 506734160..445068762 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.notification import android.app.PendingIntent import android.content.Context import android.content.Intent -import eu.kanade.tachiyomi.ui.download.DownloadActivity +import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.getUriCompat import java.io.File @@ -17,8 +17,9 @@ object NotificationHandler { * @param context context of application */ internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent { - val intent = Intent(context, DownloadActivity::class.java).apply { + val intent = Intent(context, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + action = MainActivity.SHORTCUT_DOWNLOADS } return PendingIntent.getActivity(context, 0, intent, 0) } 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 b687b8d3d..2ee14b3bb 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 @@ -1,120 +1,118 @@ -package eu.kanade.tachiyomi.data.preference - -import android.content.Context -import eu.kanade.tachiyomi.R - -/** - * This class stores the keys for the preferences in the application. Most of them are defined - * in the file "keys.xml". By using this class we can define preferences in one place and get them - * referenced here. - */ -@Suppress("HasPlatformType") -class PreferenceKeys(context: Context) { - - val theme = context.getString(R.string.pref_theme_key) - - val rotation = context.getString(R.string.pref_rotation_type_key) - - val enableTransitions = context.getString(R.string.pref_enable_transitions_key) - - val showPageNumber = context.getString(R.string.pref_show_page_number_key) - - val fullscreen = context.getString(R.string.pref_fullscreen_key) - - val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key) - - val customBrightness = context.getString(R.string.pref_custom_brightness_key) - - val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key) - - val colorFilter = context.getString(R.string.pref_color_filter_key) - - val colorFilterValue = context.getString(R.string.pref_color_filter_value_key) - - val defaultViewer = context.getString(R.string.pref_default_viewer_key) - - val imageScaleType = context.getString(R.string.pref_image_scale_type_key) - - val imageDecoder = context.getString(R.string.pref_image_decoder_key) - - val zoomStart = context.getString(R.string.pref_zoom_start_key) - - val readerTheme = context.getString(R.string.pref_reader_theme_key) - - val cropBorders = context.getString(R.string.pref_crop_borders_key) - - val readWithTapping = context.getString(R.string.pref_read_with_tapping_key) - - val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key) - - val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key) - - val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key) - - val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key) - - val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key) - - val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key) - - val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key) - - val lastUsedCategory = context.getString(R.string.pref_last_used_category_key) - - val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list) - - val enabledLanguages = context.getString(R.string.pref_source_languages) - - val backupDirectory = context.getString(R.string.pref_backup_directory_key) - - val downloadsDirectory = context.getString(R.string.pref_download_directory_key) - - val downloadThreads = context.getString(R.string.pref_download_slots_key) - - val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key) - - val numberOfBackups = context.getString(R.string.pref_backup_slots_key) - - val backupInterval = context.getString(R.string.pref_backup_interval_key) - - val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key) - - val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key) - - val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key) - - val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key) - - val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key) - - val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key) - - val filterUnread = context.getString(R.string.pref_filter_unread_key) - - val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key) - - val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key) - - val startScreen = context.getString(R.string.pref_start_screen_key) - - val downloadNew = context.getString(R.string.pref_download_new_key) - - val downloadNewCategories = context.getString(R.string.pref_download_new_categories_key) - - fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" - - fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" - - fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" - - fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" - - fun trackToken(syncId: Int) = "track_token_$syncId" - - val libraryAsList = context.getString(R.string.pref_display_library_as_list) - - val lang = context.getString(R.string.pref_language_key) - - val defaultCategory = context.getString(R.string.default_category_key) - -} +package eu.kanade.tachiyomi.data.preference + +/** + * This class stores the keys for the preferences in the application. + */ +object PreferenceKeys { + + const val theme = "pref_theme_key" + + const val rotation = "pref_rotation_type_key" + + const val enableTransitions = "pref_enable_transitions_key" + + const val showPageNumber = "pref_show_page_number_key" + + const val fullscreen = "fullscreen" + + const val keepScreenOn = "pref_keep_screen_on_key" + + const val customBrightness = "pref_custom_brightness_key" + + const val customBrightnessValue = "custom_brightness_value" + + const val colorFilter = "pref_color_filter_key" + + const val colorFilterValue = "color_filter_value" + + const val defaultViewer = "pref_default_viewer_key" + + const val imageScaleType = "pref_image_scale_type_key" + + const val imageDecoder = "image_decoder" + + const val zoomStart = "pref_zoom_start_key" + + const val readerTheme = "pref_reader_theme_key" + + const val cropBorders = "crop_borders" + + const val readWithTapping = "reader_tap" + + const val readWithVolumeKeys = "reader_volume_keys" + + const val readWithVolumeKeysInverted = "reader_volume_keys_inverted" + + const val portraitColumns = "pref_library_columns_portrait_key" + + const val landscapeColumns = "pref_library_columns_landscape_key" + + const val updateOnlyNonCompleted = "pref_update_only_non_completed_key" + + const val autoUpdateTrack = "pref_auto_update_manga_sync_key" + + const val askUpdateTrack = "pref_ask_update_manga_sync_key" + + const val lastUsedCatalogueSource = "last_catalogue_source" + + const val lastUsedCategory = "last_used_category" + + const val catalogueAsList = "pref_display_catalogue_as_list" + + const val enabledLanguages = "source_languages" + + const val backupDirectory = "backup_directory" + + const val downloadsDirectory = "download_directory" + + const val downloadThreads = "pref_download_slots_key" + + const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key" + + const val numberOfBackups = "backup_slots" + + const val backupInterval = "backup_interval" + + const val removeAfterReadSlots = "remove_after_read_slots" + + const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key" + + const val libraryUpdateInterval = "pref_library_update_interval_key" + + const val libraryUpdateRestriction = "library_update_restriction" + + const val libraryUpdateCategories = "library_update_categories" + + const val filterDownloaded = "pref_filter_downloaded_key" + + const val filterUnread = "pref_filter_unread_key" + + const val filterCompleted = "pref_filter_completed_key" + + const val librarySortingMode = "library_sorting_mode" + + const val automaticUpdates = "automatic_updates" + + const val startScreen = "start_screen" + + const val downloadNew = "download_new" + + const val downloadNewCategories = "download_new_categories" + + const val libraryAsList = "pref_display_library_as_list" + + const val lang = "app_language" + + const val defaultCategory = "default_category" + + fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" + + fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" + + fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" + + fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" + + fun trackToken(syncId: Int) = "track_token_$syncId" + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 0d6568058..46cedc1bf 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 @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.source.Source import exh.ui.migration.MigrationStatus import java.io.File +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys fun Preference.getOrDefault(): T = get() ?: defaultValue()!! @@ -18,8 +19,6 @@ fun Preference.invert(): Boolean = getOrDefault().let { set(!it); !it } class PreferencesHelper(val context: Context) { - val keys = PreferenceKeys(context) - private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val rxPrefs = RxSharedPreferences.create(prefs) @@ -31,137 +30,142 @@ class PreferencesHelper(val context: Context) { File(Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name), "backup")) - fun startScreen() = prefs.getInt(keys.startScreen, 1) + fun startScreen() = prefs.getInt(Keys.startScreen, 1) fun clear() = prefs.edit().clear().apply() - fun theme() = prefs.getInt(keys.theme, 1) + fun theme() = prefs.getInt(Keys.theme, 1) - fun rotation() = rxPrefs.getInteger(keys.rotation, 1) + fun rotation() = rxPrefs.getInteger(Keys.rotation, 1) - fun pageTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true) + fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true) - fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true) + fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true) - fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true) + fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true) - fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true) + fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true) - fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false) + fun customBrightness() = rxPrefs.getBoolean(Keys.customBrightness, false) - fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0) + fun customBrightnessValue() = rxPrefs.getInteger(Keys.customBrightnessValue, 0) - fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false) + fun colorFilter() = rxPrefs.getBoolean(Keys.colorFilter, false) - fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0) + fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0) - fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1) + fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1) - fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1) + fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1) - fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0) + fun imageDecoder() = rxPrefs.getInteger(Keys.imageDecoder, 0) - fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1) + fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1) - fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0) + fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0) - fun cropBorders() = rxPrefs.getBoolean(keys.cropBorders, false) + fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false) - fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true) + fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true) - fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false) + fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false) - fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0) + fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false) - fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0) + fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0) - fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false) + fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0) - fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true) + fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false) - fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false) + fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) - fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1) + fun askUpdateTrack() = prefs.getBoolean(Keys.askUpdateTrack, false) - fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0) + fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1) + + fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0) fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0) - fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false) + fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false) - fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("all")) + fun enabledLanguages() = rxPrefs.getStringSet(Keys.enabledLanguages, setOf("all")) - fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "") + fun sourceUsername(source: Source) = prefs.getString(Keys.sourceUsername(source.id), "") - fun sourcePassword(source: Source) = prefs.getString(keys.sourcePassword(source.id), "") + fun sourcePassword(source: Source) = prefs.getString(Keys.sourcePassword(source.id), "") fun setSourceCredentials(source: Source, username: String, password: String) { prefs.edit() - .putString(keys.sourceUsername(source.id), username) - .putString(keys.sourcePassword(source.id), password) + .putString(Keys.sourceUsername(source.id), username) + .putString(Keys.sourcePassword(source.id), password) .apply() } - fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "") + fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "") - fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "") + fun trackPassword(sync: TrackService) = prefs.getString(Keys.trackPassword(sync.id), "") fun setTrackCredentials(sync: TrackService, username: String, password: String) { prefs.edit() - .putString(keys.trackUsername(sync.id), username) - .putString(keys.trackPassword(sync.id), password) + .putString(Keys.trackUsername(sync.id), username) + .putString(Keys.trackPassword(sync.id), password) .apply() } - fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "") + fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "") fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0) - fun backupsDirectory() = rxPrefs.getString(keys.backupDirectory, defaultBackupDir.toString()) + fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString()) - fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString()) + fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString()) - fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1) + fun downloadThreads() = rxPrefs.getInteger(Keys.downloadThreads, 1) - fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true) + fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true) - fun numberOfBackups() = rxPrefs.getInteger(keys.numberOfBackups, 1) + fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1) - fun backupInterval() = rxPrefs.getInteger(keys.backupInterval, 0) + fun backupInterval() = rxPrefs.getInteger(Keys.backupInterval, 0) - fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1) + fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1) - fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false) + fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false) - fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0) + fun libraryUpdateInterval() = rxPrefs.getInteger(Keys.libraryUpdateInterval, 0) - fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet()) + fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet()) - fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet()) + fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) - fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false) + fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) - fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false) + fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false) - fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false) + fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false) - fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0) + fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false) + + fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0) fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true) - fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false) + fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, false) fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet()) - fun downloadNew() = rxPrefs.getBoolean(keys.downloadNew, false) + fun downloadNew() = rxPrefs.getBoolean(Keys.downloadNew, false) - fun downloadNewCategories() = rxPrefs.getStringSet(keys.downloadNewCategories, emptySet()) + fun downloadNewCategories() = rxPrefs.getStringSet(Keys.downloadNewCategories, emptySet()) - fun lang() = prefs.getString(keys.lang, "") + fun lang() = prefs.getString(Keys.lang, "") - fun defaultCategory() = prefs.getInt(keys.defaultCategory, -1) + fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1) - //EH + //TODO + // --> EH fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false) fun secureEXH() = rxPrefs.getBoolean("secure_exh", true) @@ -195,4 +199,5 @@ class PreferencesHelper(val context: Context) { fun lockSalt() = rxPrefs.getString("lock_salt", null) fun lockLength() = rxPrefs.getInteger("lock_length", -1) + // <-- EH } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 1e1e620cb..8f67ddbe8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -26,7 +26,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { fun addLibManga(track: Track): Observable { return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus()) .map { response -> - response.body().close() + response.body()?.close() if (!response.isSuccessful) { throw Exception("Could not add manga") } @@ -38,7 +38,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(), track.toAnilistScore()) .map { response -> - response.body().close() + response.body()?.close() if (!response.isSuccessful) { throw Exception("Could not update manga") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 7cdc5dde8..2bb8525d3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -28,7 +28,7 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor { if (oauth == null || oauth!!.isExpired()) { val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!)) oauth = if (response.isSuccessful) { - Gson().fromJson(response.body().string(), OAuth::class.java) + Gson().fromJson(response.body()!!.string(), OAuth::class.java) } else { response.close() null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 23d562ed6..895eca98d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -151,7 +151,6 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) fun findLibManga( @Query("filter[manga_id]", encoded = true) remoteId: Int, @Query("filter[user_id]", encoded = true) userId: String, - @Query("page[limit]", encoded = true) limit: Int = 10000, @Query("include") includes: String = "manga" ): Observable diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt index 92847310f..8810dd274 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt @@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor { if (currAuth.isExpired()) { val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken)) if (response.isSuccessful) { - newAuth(gson.fromJson(response.body().string(), OAuth::class.java)) + newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java)) } else { response.close() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt index 973b2f26e..85f85807e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt @@ -46,7 +46,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor } else { client.newCall(GET(getSearchUrl(query), headers)) .asObservable() - .map { Jsoup.parse(it.body().string()) } + .map { Jsoup.parse(it.body()!!.string()) } .flatMap { Observable.from(it.select("entry")) } .filter { it.select("type").text() != "Novel" } .map { @@ -64,7 +64,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor return client .newCall(GET(getListUrl(username), headers)) .asObservable() - .map { Jsoup.parse(it.body().string()) } + .map { Jsoup.parse(it.body()!!.string()) } .flatMap { Observable.from(it.select("manga")) } .map { Track.create(TrackManager.MYANIMELIST).apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt index 423513898..dfb33db5b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt @@ -86,7 +86,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav val apkFile = File(externalCacheDir, "update.apk") if (response.isSuccessful) { - response.body().source().saveTo(apkFile) + response.body()!!.source().saveTo(apkFile) } else { response.close() throw Exception("Unsuccessful response") diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index 3a1840b06..8885e6f16 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -8,13 +8,10 @@ import okhttp3.Response class CloudflareInterceptor : Interceptor { - //language=RegExp private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""") - //language=RegExp private val passPattern = Regex("""name="pass" value="(.+?)"""") - //language=RegExp private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""") @Synchronized @@ -34,7 +31,7 @@ class CloudflareInterceptor : Interceptor { val originalRequest = response.request() val url = originalRequest.url() val domain = url.host() - val content = response.body().string() + val content = response.body()!!.string() // CloudFlare requires waiting 4 seconds before resolving the challenge Thread.sleep(4000) @@ -48,9 +45,7 @@ class CloudflareInterceptor : Interceptor { } val js = operation - //language=RegExp .replace(Regex("""a\.value =(.+?) \+.*"""), "$1") - //language=RegExp .replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "") .replace("\n", "") @@ -58,7 +53,7 @@ class CloudflareInterceptor : Interceptor { val answer = "${result + domain.length}" - val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl") + val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!! .newBuilder() .addQueryParameter("jschl_vc", challenge) .addQueryParameter("pass", pass) diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index ba72fb62b..d82a75923 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -61,7 +61,7 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene .addNetworkInterceptor { chain -> val originalResponse = chain.proceed(chain.request()) originalResponse.newBuilder() - .body(ProgressResponseBody(originalResponse.body(), listener)) + .body(ProgressResponseBody(originalResponse.body()!!, listener)) .build() } .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 4664b22f4..766bb49a2 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt @@ -18,7 +18,7 @@ class PersistentCookieStore(context: Context) { if (cookies != null) { try { val url = HttpUrl.parse("http://$key") - val nonExpiredCookies = cookies.map { Cookie.parse(url, it) } + val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) } .filter { !it.hasExpired() } cookieMap.put(key, nonExpiredCookies) } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index 2e2c32f75..f8123c519 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -12,7 +12,7 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p } override fun contentType(): MediaType { - return responseBody.contentType() + return responseBody.contentType()!! } override fun contentLength(): Long { 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 d63c90844..00662ceb9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -57,7 +57,7 @@ class LocalSource(private val context: Context) : CatalogueSource { override val id = ID override val name = "LocalSource" - override val lang = "en" + override val lang = "" override val supportsLatest = true override fun toString() = context.getString(R.string.local_source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt index a54a36b40..991d24d41 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt @@ -12,11 +12,14 @@ interface SChapter : Serializable { var chapter_number: Float + var scanlator: String? + fun copyFrom(other: SChapter) { name = other.name url = other.url date_upload = other.date_upload chapter_number = other.chapter_number + scanlator = other.scanlator } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt index 026d437e0..cfc4c3999 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt @@ -10,4 +10,6 @@ class SChapterImpl : SChapter { override var chapter_number: Float = -1f + override var scanlator: String? = null + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/YamlHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/YamlHttpSource.kt index e73681d23..582a7c3d3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/YamlHttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/YamlHttpSource.kt @@ -171,7 +171,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() { } override fun pageListParse(response: Response): List { - val body = response.body().string() + val body = response.body()!!.string() val url = response.request().url().toString() val pages = mutableListOf() @@ -216,7 +216,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() { } override fun imageUrlParse(response: Response): String { - val body = response.body().string() + val body = response.body()!!.string() val url = response.request().url().toString() with(map.pages) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt index d8f35cfb2..ef7cd2f4c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt @@ -28,7 +28,7 @@ class Batoto : ParsedHttpSource(), LoginSource { override val name = "Batoto" - override val baseUrl = "http://bato.to" + override val baseUrl = "https://bato.to" override val lang = "en" @@ -52,7 +52,7 @@ class Batoto : ParsedHttpSource(), LoginSource { .add("Cookie", "lang_option=English") private val pageHeaders = super.headersBuilder() - .add("Referer", "http://bato.to/reader") + .add("Referer", "$baseUrl/reader") .build() override fun popularMangaRequest(page: Int): Request { @@ -69,7 +69,7 @@ class Batoto : ParsedHttpSource(), LoginSource { override fun popularMangaFromElement(element: Element): SManga { val manga = SManga.create() - element.select("a[href^=http://bato.to]").first().let { + element.select("a[href^=$baseUrl]").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text().trim() } @@ -85,7 +85,7 @@ class Batoto : ParsedHttpSource(), LoginSource { override fun latestUpdatesNextPageSelector() = "#show_more_row" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder() + val url = HttpUrl.parse("$baseUrl/search_ajax")!!.newBuilder() if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c") var genres = "" filters.forEach { filter -> @@ -161,8 +161,20 @@ class Batoto : ParsedHttpSource(), LoginSource { else -> SManga.UNKNOWN } + override fun chapterListRequest(manga: SManga): Request { + // Https is currently very slow. The replace also saves a redirection. + var newUrl = "http://bato.to" + manga.url + if ("/comic/_/comics/" !in newUrl) { + newUrl = newUrl.replace("/comic/_/", "/comic/_/comics/") + } + + return super.chapterListRequest(manga).newBuilder() + .url(newUrl) + .build() + } + override fun chapterListParse(response: Response): List { - val body = response.body().string() + val body = response.body()!!.string() val matcher = staffNotice.matcher(body) if (matcher.find()) { @Suppress("DEPRECATION") @@ -177,7 +189,7 @@ class Batoto : ParsedHttpSource(), LoginSource { override fun chapterListSelector() = "tr.row.lang_English.chapter_row" override fun chapterFromElement(element: Element): SChapter { - val urlElement = element.select("a[href^=http://bato.to/reader").first() + val urlElement = element.select("a[href^=$baseUrl/reader").first() val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) @@ -185,6 +197,7 @@ class Batoto : ParsedHttpSource(), LoginSource { chapter.date_upload = element.select("td").getOrNull(4)?.let { parseDateFromElement(it) } ?: 0 + chapter.scanlator = element.select("td").getOrNull(2)?.text() return chapter } @@ -271,7 +284,7 @@ class Batoto : ParsedHttpSource(), LoginSource { } override fun isAuthenticationSuccessful(response: Response) = - response.priorResponse() != null && response.priorResponse().code() == 302 + response.priorResponse() != null && response.priorResponse()!!.code() == 302 override fun isLogged(): Boolean { return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" } 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 098467d7b..b77adeca3 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 @@ -115,13 +115,13 @@ class Kissmanga : ParsedHttpSource() { override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers) override fun pageListParse(response: Response): List { - val body = response.body().string() + val body = response.body()!!.string() val pages = mutableListOf() // Kissmanga now encrypts the urls, so we need to execute these two scripts in JS. - val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body().string() - val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body().string() + val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body()!!.string() + val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body()!!.string() Duktape.create().use { it.evaluate(ca) 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 5ea36d8e0..4444d9105 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 @@ -55,7 +55,7 @@ class Mangafox : ParsedHttpSource() { override fun latestUpdatesNextPageSelector() = "a:has(span.next)" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query) + val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query) (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> when (filter) { is Status -> url.addQueryParameter(filter.id, filter.state.toString()) 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 66d76d4e1..42a8e17b9 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 @@ -57,7 +57,7 @@ class Mangahere : ParsedHttpSource() { override fun latestUpdatesNextPageSelector() = "div.next-page > a.next" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query) + val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query) (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> when (filter) { is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state]) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangasee.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangasee.kt index b0aa2a8c7..eb950a8fc 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangasee.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangasee.kt @@ -54,7 +54,7 @@ class Mangasee : ParsedHttpSource() { override fun searchMangaSelector() = "div.requested > div.row" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder() + val url = HttpUrl.parse("$baseUrl/search/request.php")!!.newBuilder() if (!query.isEmpty()) url.addQueryParameter("keyword", query) val genres = mutableListOf() val genresNo = mutableListOf() @@ -84,7 +84,7 @@ class Mangasee : ParsedHttpSource() { } private fun convertQueryToPost(page: Int, url: String): Pair { - val url = HttpUrl.parse(url) + val url = HttpUrl.parse(url)!! val body = FormBody.Builder().add("page", page.toString()) for (i in 0..url.querySize() - 1) { body.add(url.queryParameterName(i), url.queryParameterValue(i)) 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 0868e4cdc..a104f1b37 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 @@ -152,7 +152,7 @@ class Mangachan : ParsedHttpSource() { } override fun pageListParse(response: Response): List { - val html = response.body().string() + val html = response.body()!!.string() val beginIndex = html.indexOf("fullimg\":[") + 10 val endIndex = html.indexOf(",]", beginIndex) val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "") 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 6097859fa..b86102356 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 @@ -120,7 +120,7 @@ class Mintmanga : ParsedHttpSource() { } override fun pageListParse(response: Response): List { - val html = response.body().string() + val html = response.body()!!.string() val beginIndex = html.indexOf("rm_h.init( [") val endIndex = html.indexOf("], 0, false);", beginIndex) val trimmedHtml = html.substring(beginIndex, endIndex) 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 6ac6e9c28..bb3af34dc 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 @@ -120,7 +120,7 @@ class Readmanga : ParsedHttpSource() { } override fun pageListParse(response: Response): List { - val html = response.body().string() + val html = response.body()!!.string() val beginIndex = html.indexOf("rm_h.init( [") val endIndex = html.indexOf("], 0, false);", beginIndex) val trimmedHtml = html.substring(beginIndex, endIndex) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/ActivityMixin.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/ActivityMixin.kt deleted file mode 100755 index ea1da77e9..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/ActivityMixin.kt +++ /dev/null @@ -1,83 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.activity - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build -import android.support.v4.app.ActivityCompat -import android.support.v4.content.ContextCompat -import android.support.v7.app.ActionBar -import android.support.v7.app.AppCompatActivity -import android.support.v7.widget.Toolbar -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -interface ActivityMixin { - - var resumed: Boolean - - fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) { - setSupportActionBar(toolbar) - getSupportActionBar()?.setDisplayHomeAsUpEnabled(true) - if (backNavigation) { - toolbar.setNavigationOnClickListener { - if (resumed) { - onBackPressed() - } - } - } - } - - fun setAppTheme() { - setTheme(when (Injekt.get().theme()) { - 2 -> R.style.Theme_Tachiyomi_Dark - else -> R.style.Theme_Tachiyomi - }) - } - - fun setToolbarTitle(title: String) { - getSupportActionBar()?.title = title - } - - fun setToolbarTitle(titleResource: Int) { - getSupportActionBar()?.title = getString(titleResource) - } - - fun setToolbarSubtitle(title: String) { - getSupportActionBar()?.subtitle = title - } - - fun setToolbarSubtitle(titleResource: Int) { - getSupportActionBar()?.subtitle = getString(titleResource) - } - - /** - * Requests read and write permissions on Android M and higher. - */ - fun requestPermissionsOnMarshmallow() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(getActivity(), - Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - - ActivityCompat.requestPermissions(getActivity(), - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), - 1) - - } - } - } - - fun getActivity(): AppCompatActivity - - fun onBackPressed() - - fun getSupportActionBar(): ActionBar? - - fun setSupportActionBar(toolbar: Toolbar?) - - fun setTheme(resource: Int) - - fun getString(resource: Int): String - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index 59a2c317a..163d45345 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -2,36 +2,14 @@ package eu.kanade.tachiyomi.ui.base.activity import android.support.v7.app.AppCompatActivity import eu.kanade.tachiyomi.util.LocaleHelper -import exh.ui.lock.lockEnabled -import exh.ui.lock.showLockActivity -import android.app.ActivityManager -import android.app.Service -import android.app.usage.UsageStats -import android.app.usage.UsageStatsManager -import android.os.Build -import java.util.* - -abstract class BaseActivity : AppCompatActivity(), ActivityMixin { - - override var resumed = false +abstract class BaseActivity : AppCompatActivity() { init { + @Suppress("LeakingThis") LocaleHelper.updateConfiguration(this) } - override fun getActivity() = this - - override fun onResume() { - super.onResume() - resumed = true - } - - override fun onPause() { - resumed = false - super.onPause() - } - var willLock = false var disableLock = false override fun onRestart() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt index 71e598ded..560fb01c8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt @@ -1,40 +1,14 @@ package eu.kanade.tachiyomi.ui.base.activity -import android.os.Bundle -import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.LocaleHelper import nucleus.view.NucleusAppCompatActivity -abstract class BaseRxActivity

> : NucleusAppCompatActivity

(), ActivityMixin { - - override var resumed = false +abstract class BaseRxActivity

> : NucleusAppCompatActivity

() { init { + @Suppress("LeakingThis") LocaleHelper.updateConfiguration(this) } - override fun onCreate(savedState: Bundle?) { - val superFactory = presenterFactory - setPresenterFactory { - superFactory.createPresenter().apply { - val app = application as App - context = app.applicationContext - } - } - super.onCreate(savedState) - } - - override fun getActivity() = this - - override fun onResume() { - super.onResume() - resumed = true - } - - override fun onPause() { - resumed = false - super.onPause() - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt deleted file mode 100755 index f2c41cc05..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt +++ /dev/null @@ -1,39 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.adapter - -import android.support.v7.widget.RecyclerView -import android.view.View - -import eu.davidea.flexibleadapter4.FlexibleAdapter - -abstract class FlexibleViewHolder(view: View, - private val adapter: FlexibleAdapter<*, *>, - private val itemClickListener: FlexibleViewHolder.OnListItemClickListener) : - RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener { - - init { - view.setOnClickListener(this) - view.setOnLongClickListener(this) - } - - override fun onClick(view: View) { - if (itemClickListener.onListItemClick(adapterPosition)) { - toggleActivation() - } - } - - override fun onLongClick(view: View): Boolean { - itemClickListener.onListItemLongClick(adapterPosition) - toggleActivation() - return true - } - - fun toggleActivation() { - itemView.isActivated = adapter.isSelected(adapterPosition) - } - - interface OnListItemClickListener { - fun onListItemClick(position: Int): Boolean - fun onListItemLongClick(position: Int) - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/SmartFragmentStatePagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/SmartFragmentStatePagerAdapter.kt deleted file mode 100755 index 2169b603c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/SmartFragmentStatePagerAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.adapter - -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager -import android.support.v4.app.FragmentStatePagerAdapter -import android.util.SparseArray -import android.view.ViewGroup -import java.util.* - -abstract class SmartFragmentStatePagerAdapter(fragmentManager: FragmentManager) : - FragmentStatePagerAdapter(fragmentManager) { - // Sparse array to keep track of registered fragments in memory - private val registeredFragments = SparseArray() - - // Register the fragment when the item is instantiated - override fun instantiateItem(container: ViewGroup, position: Int): Any { - val fragment = super.instantiateItem(container, position) as Fragment - registeredFragments.put(position, fragment) - return fragment - } - - // Unregister when the item is inactive - override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any) { - registeredFragments.remove(position) - super.destroyItem(container, position, `object`) - } - - // Returns the fragment for the position (if instantiated) - fun getRegisteredFragment(position: Int): Fragment { - return registeredFragments.get(position) - } - - fun getRegisteredFragments(): List { - val fragments = ArrayList() - for (i in 0..registeredFragments.size() - 1) { - fragments.add(registeredFragments.valueAt(i)) - } - return fragments - } - -} \ No newline at end of file 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 new file mode 100644 index 000000000..9f55cd033 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt @@ -0,0 +1,68 @@ +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 +import android.view.View +import android.view.ViewGroup +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.bluelinelabs.conductor.RestoreViewOnCreateController + +abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { + val view = inflateView(inflater, container) + onViewCreated(view, savedViewState) + return view + } + + abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View + + open fun onViewCreated(view: View, savedViewState: Bundle?) { } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + if (type.isEnter) { + setTitle() + } + super.onChangeStarted(handler, type) + } + + open fun getTitle(): String? { + return null + } + + private fun setTitle() { + var parentController = parentController + while (parentController != null) { + if (parentController is BaseController && parentController.getTitle() != null) { + return + } + parentController = parentController.parentController + } + + (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() + } + + /** + * Workaround for disappearing menu items when collapsing an expandable item like a SearchView. + * This method should be removed when fixed upstream. + * Issue link: https://issuetracker.google.com/issues/37657375 + */ + fun MenuItem.fixExpand() { + val expandListener = object : MenuItemCompat.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + 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/ConductorExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt new file mode 100644 index 000000000..19150e132 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Build +import android.support.v4.content.ContextCompat +import com.bluelinelabs.conductor.Controller +import com.bluelinelabs.conductor.Router + +fun Router.popControllerWithTag(tag: String): Boolean { + val controller = getControllerWithTag(tag) + if (controller != null) { + popController(controller) + return true + } + return false +} + +fun Controller.requestPermissionsSafe(permissions: Array, requestCode: Int) { + val activity = activity ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + permissions.forEach { permission -> + if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) { + requestPermissions(arrayOf(permission), requestCode) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java new file mode 100644 index 000000000..db8efbd83 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java @@ -0,0 +1,139 @@ +package eu.kanade.tachiyomi.ui.base.controller; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.RestoreViewOnCreateController; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler; + +/** + * A controller that displays a dialog window, floating on top of its activity's window. + * This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}. + * + *

Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog} + */ +public abstract class DialogController extends RestoreViewOnCreateController { + + private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; + + private Dialog dialog; + private boolean dismissed; + + /** + * Convenience constructor for use when no arguments are needed. + */ + protected DialogController() { + super(null); + } + + /** + * Constructor that takes arguments that need to be retained across restarts. + * + * @param args Any arguments that need to be retained. + */ + protected DialogController(@Nullable Bundle args) { + super(args); + } + + @NonNull + @Override + final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) { + dialog = onCreateDialog(savedViewState); + //noinspection ConstantConditions + dialog.setOwnerActivity(getActivity()); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + dismissDialog(); + } + }); + if (savedViewState != null) { + Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG); + if (dialogState != null) { + dialog.onRestoreInstanceState(dialogState); + } + } + return new View(getActivity());//stub view + } + + @Override + protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { + super.onSaveViewState(view, outState); + Bundle dialogState = dialog.onSaveInstanceState(); + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); + } + + @Override + protected void onAttach(@NonNull View view) { + super.onAttach(view); + dialog.show(); + } + + @Override + protected void onDetach(@NonNull View view) { + super.onDetach(view); + dialog.hide(); + } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + dialog.setOnDismissListener(null); + dialog.dismiss(); + dialog = null; + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * @param router The router on which the transaction will be applied + */ + public void showDialog(@NonNull Router router) { + showDialog(router, null); + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * @param router The router on which the transaction will be applied + * @param tag The tag for this controller + */ + public void showDialog(@NonNull Router router, @Nullable String tag) { + dismissed = false; + router.pushController(RouterTransaction.with(this) + .pushChangeHandler(new SimpleSwapChangeHandler(false)) + .popChangeHandler(new SimpleSwapChangeHandler(false)) + .tag(tag)); + } + + /** + * Dismiss the dialog and pop this controller + */ + public void dismissDialog() { + if (dismissed) { + return; + } + getRouter().popController(this); + dismissed = true; + } + + @Nullable + protected Dialog getDialog() { + return dialog; + } + + /** + * Build your own custom Dialog container such as an {@link android.app.AlertDialog} + * + * @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists. + * @return Return a new Dialog instance to be displayed by the Controller + */ + @NonNull + protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState); +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt new file mode 100644 index 000000000..c03612389 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt @@ -0,0 +1,3 @@ +package eu.kanade.tachiyomi.ui.base.controller + +interface NoToolbarElevationController \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt new file mode 100644 index 000000000..63eba25ed --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.os.Bundle +import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate +import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener +import nucleus.factory.PresenterFactory +import nucleus.presenter.Presenter + +@Suppress("LeakingThis") +abstract class NucleusController

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

{ + + private val delegate = NucleusConductorDelegate(this) + + val presenter: P + get() = delegate.presenter + + init { + addLifecycleListener(NucleusConductorLifecycleListener(delegate)) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java new file mode 100644 index 000000000..cf265fc3d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java @@ -0,0 +1,186 @@ +package eu.kanade.tachiyomi.ui.base.controller; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.PagerAdapter; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; + +import java.util.ArrayList; +import java.util.List; + +/** + * An adapter for ViewPagers that uses Routers as pages + */ +public abstract class RouterPagerAdapter extends PagerAdapter { + + private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates"; + private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave"; + private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory"; + + private final Controller host; + private int maxPagesToStateSave = Integer.MAX_VALUE; + private SparseArray savedPages = new SparseArray<>(); + private SparseArray visibleRouters = new SparseArray<>(); + private ArrayList savedPageHistory = new ArrayList<>(); + private Router primaryRouter; + + /** + * Creates a new RouterPagerAdapter using the passed host. + */ + public RouterPagerAdapter(@NonNull Controller host) { + this.host = host; + } + + /** + * Called when a router is instantiated. Here the router's root should be set if needed. + * + * @param router The router used for the page + * @param position The page position to be instantiated. + */ + public abstract void configureRouter(@NonNull Router router, int position); + + /** + * Sets the maximum number of pages that will have their states saved. When this number is exceeded, + * the page that was state saved least recently will have its state removed from the save data. + */ + public void setMaxPagesToStateSave(int maxPagesToStateSave) { + if (maxPagesToStateSave < 0) { + throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave."); + } + + this.maxPagesToStateSave = maxPagesToStateSave; + + ensurePagesSaved(); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + final String name = makeRouterName(container.getId(), getItemId(position)); + + Router router = host.getChildRouter(container, name); + if (!router.hasRootController()) { + Bundle routerSavedState = savedPages.get(position); + + if (routerSavedState != null) { + router.restoreInstanceState(routerSavedState); + savedPages.remove(position); + } + } + + router.rebindIfNeeded(); + configureRouter(router, position); + + if (router != primaryRouter) { + for (RouterTransaction transaction : router.getBackstack()) { + transaction.controller().setOptionsMenuHidden(true); + } + } + + visibleRouters.put(position, router); + return router; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Router router = (Router)object; + + Bundle savedState = new Bundle(); + router.saveInstanceState(savedState); + savedPages.put(position, savedState); + + savedPageHistory.remove((Integer)position); + savedPageHistory.add(position); + + ensurePagesSaved(); + + host.removeChildRouter(router); + + visibleRouters.remove(position); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Router router = (Router)object; + if (router != primaryRouter) { + if (primaryRouter != null) { + for (RouterTransaction transaction : primaryRouter.getBackstack()) { + transaction.controller().setOptionsMenuHidden(true); + } + } + if (router != null) { + for (RouterTransaction transaction : router.getBackstack()) { + transaction.controller().setOptionsMenuHidden(false); + } + } + primaryRouter = router; + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + Router router = (Router)object; + final List backstack = router.getBackstack(); + for (RouterTransaction transaction : backstack) { + if (transaction.controller().getView() == view) { + return true; + } + } + return false; + } + + @Override + public Parcelable saveState() { + Bundle bundle = new Bundle(); + bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages); + bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave); + bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory); + return bundle; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + Bundle bundle = (Bundle)state; + if (state != null) { + savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES); + maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE); + savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY); + } + } + + /** + * Returns the already instantiated Router in the specified position or {@code null} if there + * is no router associated with this position. + */ + @Nullable + public Router getRouter(int position) { + return visibleRouters.get(position); + } + + public long getItemId(int position) { + return position; + } + + SparseArray getSavedPages() { + return savedPages; + } + + private void ensurePagesSaved() { + while (savedPages.size() > maxPagesToStateSave) { + int positionToRemove = savedPageHistory.remove(0); + savedPages.remove(positionToRemove); + } + } + + private static String makeRouterName(int viewId, long id) { + return viewId + ":" + id; + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt new file mode 100644 index 000000000..80d3b31d4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt @@ -0,0 +1,92 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.os.Bundle +import android.support.annotation.CallSuper +import android.view.View +import rx.Observable +import rx.Subscription +import rx.subscriptions.CompositeSubscription + +abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) { + + var untilDetachSubscriptions = CompositeSubscription() + private set + + var untilDestroySubscriptions = CompositeSubscription() + private set + + @CallSuper + override fun onAttach(view: View) { + super.onAttach(view) + if (untilDetachSubscriptions.isUnsubscribed) { + untilDetachSubscriptions = CompositeSubscription() + } + } + + @CallSuper + override fun onDetach(view: View) { + super.onDetach(view) + untilDetachSubscriptions.unsubscribe() + } + + @CallSuper + override fun onViewCreated(view: View, savedViewState: Bundle?) { + if (untilDestroySubscriptions.isUnsubscribed) { + untilDestroySubscriptions = CompositeSubscription() + } + } + + @CallSuper + override fun onDestroyView(view: View) { + super.onDestroyView(view) + untilDestroySubscriptions.unsubscribe() + } + + + fun Observable.subscribeUntilDetach(): Subscription { + + return subscribe().also { untilDetachSubscriptions.add(it) } + } + + fun Observable.subscribeUntilDetach(onNext: (T) -> Unit): Subscription { + + return subscribe(onNext).also { untilDetachSubscriptions.add(it) } + } + + fun Observable.subscribeUntilDetach(onNext: (T) -> Unit, + onError: (Throwable) -> Unit): Subscription { + + return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) } + } + + fun Observable.subscribeUntilDetach(onNext: (T) -> Unit, + onError: (Throwable) -> Unit, + onCompleted: () -> Unit): Subscription { + + return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) } + } + + fun Observable.subscribeUntilDestroy(): Subscription { + + return subscribe().also { untilDestroySubscriptions.add(it) } + } + + fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { + + return subscribe(onNext).also { untilDestroySubscriptions.add(it) } + } + + fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit, + onError: (Throwable) -> Unit): Subscription { + + return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) } + } + + fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit, + onError: (Throwable) -> Unit, + onCompleted: () -> Unit): Subscription { + + return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt new file mode 100644 index 000000000..ba2ce016a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.support.v4.widget.DrawerLayout +import android.view.ViewGroup + +interface SecondaryDrawerController { + + fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? + + fun cleanupSecondaryDrawer(drawer: DrawerLayout) +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt new file mode 100644 index 000000000..02fba36c3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.support.design.widget.TabLayout + +interface TabbedController { + + fun configureTabs(tabs: TabLayout) {} + + fun cleanupTabs(tabs: TabLayout) {} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt deleted file mode 100755 index ee466c536..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.fragment - -import android.support.v4.app.Fragment - -abstract class BaseFragment : Fragment(), FragmentMixin { - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt deleted file mode 100755 index 672c44731..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.fragment - -import android.os.Bundle -import eu.kanade.tachiyomi.App -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import nucleus.view.NucleusSupportFragment - -abstract class BaseRxFragment

> : NucleusSupportFragment

(), FragmentMixin { - - override fun onCreate(savedState: Bundle?) { - val superFactory = presenterFactory - setPresenterFactory { - superFactory.createPresenter().apply { - val app = activity.application as App - context = app.applicationContext - } - } - super.onCreate(savedState) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt deleted file mode 100755 index 24c766182..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt +++ /dev/null @@ -1,19 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.fragment - -import android.support.v4.app.FragmentActivity -import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin - -interface FragmentMixin { - - fun setToolbarTitle(title: String) { - (getActivity() as ActivityMixin).setToolbarTitle(title) - } - - fun setToolbarTitle(resourceId: Int) { - (getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId)) - } - - fun getActivity(): FragmentActivity - - fun getString(resource: Int): String -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index fbf756a5b..1d365b43d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -1,13 +1,9 @@ package eu.kanade.tachiyomi.ui.base.presenter -import android.content.Context import nucleus.presenter.RxPresenter -import nucleus.view.ViewWithPresenter import rx.Observable -open class BasePresenter> : RxPresenter() { - - lateinit var context: Context +open class BasePresenter : RxPresenter() { /** * Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle 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 new file mode 100644 index 000000000..62a50af83 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.ui.base.presenter; + +import android.os.Bundle; +import android.support.annotation.Nullable; + +import nucleus.factory.PresenterFactory; +import nucleus.presenter.Presenter; + +public class NucleusConductorDelegate

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

factory; + + public NucleusConductorDelegate(PresenterFactory

creator) { + this.factory = creator; + } + + public P getPresenter() { + if (presenter == null) { + presenter = factory.createPresenter(); + presenter.create(bundle); + } + bundle = null; + return presenter; + } + + Bundle onSaveInstanceState() { + Bundle bundle = new Bundle(); + getPresenter(); + if (presenter != null) { + presenter.save(bundle); + } + return bundle; + } + + 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) { + //noinspection unchecked + presenter.takeView(view); + presenterHasView = true; + } + } + + void onDropView() { + if (presenter != null && presenterHasView) { + presenter.dropView(); + presenterHasView = false; + } + } + + void onDestroy() { + if (presenter != null) { + presenter.destroy(); + presenter = null; + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java new file mode 100644 index 000000000..33272a1b2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java @@ -0,0 +1,44 @@ +package eu.kanade.tachiyomi.ui.base.presenter; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.view.View; + +import com.bluelinelabs.conductor.Controller; + +public class NucleusConductorLifecycleListener extends Controller.LifecycleListener { + + private static final String PRESENTER_STATE_KEY = "presenter_state"; + + private NucleusConductorDelegate delegate; + + public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) { + this.delegate = delegate; + } + + @Override + public void postCreateView(@NonNull Controller controller, @NonNull View view) { + delegate.onTakeView(controller); + } + + @Override + public void preDestroyView(@NonNull Controller controller, @NonNull View view) { + delegate.onDropView(); + } + + @Override + public void preDestroy(@NonNull Controller controller) { + delegate.onDestroy(); + } + + @Override + public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) { + outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState()); + } + + @Override + public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) { + delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)); + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt similarity index 54% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt index a14dc0c42..cac5a12a9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -1,609 +1,558 @@ -package eu.kanade.tachiyomi.ui.catalogue - -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.ArrayAdapter -import android.widget.ProgressBar -import android.widget.Spinner -import com.afollestad.materialdialogs.MaterialDialog -import com.f2prateek.rx.preferences.Preference -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.model.FilterList -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.util.connectivityManager -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.util.snack -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.fragment_catalogue.* -import kotlinx.android.synthetic.main.toolbar.* -import nucleus.factory.RequiresPresenter -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.subjects.PublishSubject -import uy.kohesive.injekt.injectLazy -import java.util.concurrent.TimeUnit.MILLISECONDS - -/** - * Fragment that shows the manga from the catalogue. - * Uses R.layout.fragment_catalogue. - */ -@RequiresPresenter(CataloguePresenter::class) -open class CatalogueFragment : BaseRxFragment(), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.EndlessScrollListener { - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * Spinner shown in the toolbar to change the selected source. - */ - private var spinner: Spinner? = null - - /** - * Adapter containing the list of manga from the catalogue. - */ - private lateinit var adapter: FlexibleAdapter> - - /** - * Query of the search box. - */ - private val query: String - get() = presenter.query - - /** - * Selected index of the spinner (selected source). - */ - private var selectedIndex: Int = 0 - - /** - * Time in milliseconds to wait for input events in the search query before doing network calls. - */ - private val SEARCH_TIMEOUT = 1000L - - /** - * Subject to debounce the query. - */ - private val queryDebouncerSubject = PublishSubject.create() - - /** - * Subscription of the debouncer subject. - */ - private var queryDebouncerSubscription: Subscription? = null - - /** - * Subscription of the number of manga per row. - */ - private var numColumnsSubscription: Subscription? = null - - /** - * Search item. - */ - private var searchItem: MenuItem? = null - - /** - * Property to get the toolbar from the containing activity. - */ - private val toolbar: Toolbar - get() = (activity as MainActivity).toolbar - - /** - * Snackbar containing an error message when a request fails. - */ - private var snack: Snackbar? = null - - /** - * Navigation view containing filter items. - */ - private var navView: CatalogueNavigationView? = null - - /** - * Drawer listener to allow swipe only for closing the drawer. - */ - private val drawerListener by lazy { - object : DrawerLayout.SimpleDrawerListener() { - override fun onDrawerClosed(drawerView: View) { - if (drawerView == navView) { - activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) - } - } - - override fun onDrawerOpened(drawerView: View) { - if (drawerView == navView) { - activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView) - } - } - } - } - - lateinit var recycler: RecyclerView - - private var progressItem: ProgressItem? = null - - companion object { - /** - * Creates a new instance of this fragment. - * - * @return a new instance of [CatalogueFragment]. - */ - fun newInstance(): CatalogueFragment { - return CatalogueFragment() - } - } - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_catalogue, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - // Initialize adapter, scroll listener and recycler views - adapter = FlexibleAdapter(null, this) - setupRecycler() - - // 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.spinner_item) - - val onItemSelected = IgnoreFirstSpinnerListener { position -> - val source = spinnerAdapter.getItem(position) - if (!presenter.isValidSource(source)) { - spinner?.setSelection(selectedIndex) - context.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) - onItemSelectedListener = onItemSelected - } - - setToolbarTitle("") - toolbar.addView(spinner) - - // Inflate and prepare drawer - val navView = activity.drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView - this.navView = navView - activity.drawer.addView(navView) - activity.drawer.addDrawerListener(drawerListener) - navView.setFilters(presenter.filterItems) - - navView.post { - if (isAdded && !activity.drawer.isDrawerOpen(navView)) - activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) - } - - navView.onSearchClicked = { - val allDefault = presenter.sourceFilters == presenter.source.getFilterList() - showProgressBar() - adapter.clear() - presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) - } - - navView.onResetClicked = { - presenter.appliedFilters = FilterList() - val newFilters = presenter.source.getFilterList() - presenter.sourceFilters = newFilters - navView.setFilters(presenter.filterItems) - } - - showProgressBar() - } - - private fun setupRecycler() { - if (!isAdded) return - - numColumnsSubscription?.unsubscribe() - - val oldRecycler = catalogue_view.getChildAt(1) - var oldPosition = RecyclerView.NO_POSITION - if (oldRecycler is RecyclerView) { - oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() - oldRecycler.adapter = null - - catalogue_view.removeView(oldRecycler) - } - - recycler = if (presenter.isListMode) { - RecyclerView(context).apply { - layoutManager = LinearLayoutManager(context) - addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) - } - } else { - (catalogue_view.inflate(R.layout.recycler_autofit) as AutofitRecyclerView).apply { - numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() - .doOnNext { spanCount = it } - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribe { adapter = this@CatalogueFragment.adapter } - - (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when (adapter?.getItemViewType(position)) { - R.layout.item_catalogue_grid, null -> 1 - else -> spanCount - } - } - } - } - } - recycler.setHasFixedSize(true) - recycler.adapter = adapter - - catalogue_view.addView(recycler, 1) - - if (oldPosition != RecyclerView.NO_POSITION) { - recycler.layoutManager.scrollToPosition(oldPosition) - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.catalogue_list, menu) - - // Initialize search menu - searchItem = menu.findItem(R.id.action_search).apply { - val searchView = actionView as SearchView - - if (!query.isBlank()) { - expandActionView() - searchView.setQuery(query, true) - searchView.clearFocus() - } - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - onSearchEvent(query, true) - return true - } - - override fun onQueryTextChange(newText: String): Boolean { - onSearchEvent(newText, false) - return true - } - }) - } - - // Setup filters button - menu.findItem(R.id.action_set_filter).apply { - icon.mutate() - if (presenter.sourceFilters.isEmpty()) { - isEnabled = false - icon.alpha = 128 - } else { - isEnabled = true - icon.alpha = 255 - } - } - - // Show next display mode - menu.findItem(R.id.action_display_mode).apply { - val icon = if (presenter.isListMode) - R.drawable.ic_view_module_white_24dp - else - R.drawable.ic_view_list_white_24dp - setIcon(icon) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_display_mode -> swapDisplayMode() - R.id.action_set_filter -> navView?.let { activity.drawer.openDrawer(Gravity.END) } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onResume() { - super.onResume() - queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { searchWithQuery(it) } - } - - override fun onPause() { - queryDebouncerSubscription?.unsubscribe() - super.onPause() - } - - override fun onDestroyView() { - navView?.let { - activity.drawer.removeDrawerListener(drawerListener) - activity.drawer.removeView(it) - } - numColumnsSubscription?.unsubscribe() - searchItem?.let { - if (it.isActionViewExpanded) it.collapseActionView() - } - spinner?.let { toolbar.removeView(it) } - super.onDestroyView() - } - - /** - * Called when the input text changes or is submitted. - * - * @param query the new query. - * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT]. - */ - private fun onSearchEvent(query: String, now: Boolean) { - if (now) { - searchWithQuery(query) - } else { - queryDebouncerSubject.onNext(query) - } - } - - /** - * Restarts the request with a new query. - * - * @param newQuery the new query. - */ - private fun searchWithQuery(newQuery: String) { - // If text didn't change, do nothing - if (query == newQuery) - return - - showProgressBar() - adapter.clear() - - presenter.restartPager(newQuery) - } - - /** - * Called from the presenter when the network request is received. - * - * @param page the current page. - * @param mangas the list of manga of the page. - */ - fun onAddPage(page: Int, mangas: List) { - hideProgressBar() - if (page == 1) { - adapter.clear() - resetProgressItem() - } - adapter.onLoadMoreComplete(mangas) - } - - /** - * Called from the presenter when the network request fails. - * - * @param error the error received. - */ - fun onAddPageError(error: Throwable) { - adapter.onLoadMoreComplete(null) - hideProgressBar() - - val message = if (error is NoResultsException) "No results found" else (error.message ?: "") - - snack?.dismiss() - snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_retry) { - // If not the first page, show bottom progress bar. - if (adapter.mainItemCount > 0) { - val item = progressItem ?: return@setAction - adapter.addScrollableFooterWithDelay(item, 0, true) - } else { - showProgressBar() - } - presenter.requestNext() - } - } - } - - /** - * Sets a new progress item and reenables the scroll listener. - */ - private fun resetProgressItem() { - progressItem = ProgressItem() - adapter.endlessTargetCount = 0 - adapter.setEndlessScrollListener(this, progressItem!!) - } - - /** - * Called by the adapter when scrolled near the bottom. - */ - override fun onLoadMore(lastPosition: Int, currentPage: Int) { - if (presenter.hasNextPage()) { - presenter.requestNext() - } else { - adapter.onLoadMoreComplete(null) - adapter.endlessTargetCount = 1 - } - } - - override fun noMoreLoad(newItemsSize: Int) { - } - - /** - * Called from the presenter when a manga is initialized. - * - * @param manga the manga initialized - */ - fun onMangaInitialized(manga: Manga) { - getHolder(manga)?.setImage(manga) - } - - /** - * Swaps the current display mode. - */ - fun swapDisplayMode() { - if (!isAdded) return - - presenter.swapDisplayMode() - val isListMode = presenter.isListMode - activity.invalidateOptionsMenu() - setupRecycler() - if (!isListMode || !context.connectivityManager.isActiveNetworkMetered) { - // Initialize mangas if going to grid view or if over wifi when going to list view - val mangas = (0..adapter.itemCount-1).mapNotNull { - (adapter.getItem(it) as? CatalogueItem)?.manga - } - presenter.initializeMangas(mangas) - } - } - - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) - presenter.prefs.portraitColumns() - else - presenter.prefs.landscapeColumns() - } - - /** - * 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): CatalogueHolder? { - adapter.allBoundViewHolders.forEach { holder -> - val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem - if (item != null && item.manga.id!! == manga.id!!) { - return holder as CatalogueHolder - } - } - - return null - } - - /** - * Shows the progress bar. - */ - private fun showProgressBar() { - progress.visibility = ProgressBar.VISIBLE - snack?.dismiss() - snack = null - } - - /** - * Hides active progress bars. - */ - private fun hideProgressBar() { - progress.visibility = ProgressBar.GONE - } - - /** - * Called when a manga is clicked. - * - * @param position the position of the element clicked. - * @return true if the item should be selected, false otherwise. - */ - override fun onItemClick(position: Int): Boolean { - val item = adapter.getItem(position) as? CatalogueItem ?: return false - - val intent = MangaActivity.newIntent(activity, item.manga, true) - startActivity(intent) - return false - } - - /** - * Called when a manga is long clicked. - * - * Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga - * in, the list consists of the default category plus the user's categories. The default category is preselected on - * new manga, and on already favorited manga the manga's categories are preselected. - * - * @param position the position of the element clicked. - */ - override fun onItemLongClick(position: Int) { - // Get manga - val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return - // Fetch categories - val categories = presenter.getCategories() - - if (manga.favorite){ - MaterialDialog.Builder(activity) - .items(getString(R.string.remove_from_library )) - .itemsCallback { _, _, which, _ -> - when (which) { - 0 -> { - presenter.changeMangaFavorite(manga) - adapter.notifyItemChanged(position) - } - } - }.show() - }else{ - val defaultCategory = categories.find { it.id == preferences.defaultCategory()} - if(defaultCategory != null) { - presenter.changeMangaFavorite(manga) - presenter.moveMangaToCategory(defaultCategory, manga) - // Show manga has been added - context.toast(R.string.added_to_library) - adapter.notifyItemChanged(position) - } else { - MaterialDialog.Builder(activity) - .title(R.string.action_move_category) - .items(categories.map { it.name }) - .itemsCallbackMultiChoice(presenter.getMangaCategoryIds(manga)) { dialog, position, _ -> - if (position.contains(0) && position.count() > 1) { - // Deselect default category - dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray()) - dialog.context.toast(R.string.invalid_combination) - } - true - } - .alwaysCallMultiChoiceCallback() - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog, _ -> - val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList() - updateMangaCategories(manga, selectedCategories, position) - } - .build() - .show() - } - } - } - - /** - * Update manga to use selected categories. - * - * @param manga needed to change - * @param selectedCategories selected categories - * @param position position of adapter - */ - private fun updateMangaCategories(manga: Manga, selectedCategories: List, position: Int) { - presenter.updateMangaCategories(manga,selectedCategories) - adapter.notifyItemChanged(position) - } - -} +package eu.kanade.tachiyomi.ui.catalogue + +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.model.FilterList +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.* +import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener +import kotlinx.android.synthetic.main.catalogue_controller.view.* +import kotlinx.android.synthetic.main.main_activity.* +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.subscriptions.Subscriptions +import timber.log.Timber +import uy.kohesive.injekt.injectLazy +import java.util.concurrent.TimeUnit + +/** + * Controller to manage the catalogues available in the app. + */ +open class CatalogueController(bundle: Bundle? = null) : + NucleusController(bundle), + SecondaryDrawerController, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + FlexibleAdapter.EndlessScrollListener, + ChangeMangaCategoriesDialog.Listener { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + /** + * Adapter containing the list of manga from the catalogue. + */ + 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. + */ + private var snack: Snackbar? = null + + /** + * Navigation view containing filter items. + */ + private var navView: CatalogueNavigationView? = null + + /** + * Recycler view with the list of results. + */ + private var recycler: RecyclerView? = null + + 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 + + private var numColumnsSubscription: Subscription? = null + + private var progressItem: ProgressItem? = null + + init { + setHasOptionsMenu(true) + } + + override fun getTitle(): String? { + return "" + } + + override fun createPresenter(): CataloguePresenter { + return CataloguePresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.catalogue_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + // Initialize adapter, scroll listener and recycler views + 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) + + 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 + } + + override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { + // Inflate and prepare drawer + val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView + this.navView = navView + drawerListener = DrawerSwipeCloseListener(drawer, navView).also { + drawer.addDrawerListener(it) + } + navView.setFilters(presenter.filterItems) + + navView.post { + if (isAttached && !drawer.isDrawerOpen(navView)) + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) + } + + navView.onSearchClicked = { + val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + showProgressBar() + adapter?.clear() + presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) + } + + navView.onResetClicked = { + presenter.appliedFilters = FilterList() + val newFilters = presenter.source.getFilterList() + presenter.sourceFilters = newFilters + navView.setFilters(presenter.filterItems) + } + return navView + } + + override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { + drawerListener?.let { drawer.removeDrawerListener(it) } + drawerListener = null + navView = null + } + + private fun setupRecycler(view: View) { + numColumnsSubscription?.unsubscribe() + + var oldPosition = RecyclerView.NO_POSITION + val oldRecycler = view.catalogue_view?.getChildAt(1) + if (oldRecycler is RecyclerView) { + oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + oldRecycler.adapter = null + + view.catalogue_view?.removeView(oldRecycler) + } + + val recycler = if (presenter.isListMode) { + RecyclerView(view.context).apply { + layoutManager = LinearLayoutManager(context) + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + } else { + (view.catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { + numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() + .doOnNext { spanCount = it } + .skip(1) + // Set again the adapter to recalculate the covers height + .subscribe { adapter = this@CatalogueController.adapter } + + (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return when (adapter?.getItemViewType(position)) { + R.layout.catalogue_grid_item, null -> 1 + else -> spanCount + } + } + } + } + } + recycler.setHasFixedSize(true) + recycler.adapter = adapter + + view.catalogue_view.addView(recycler, 1) + + if (oldPosition != RecyclerView.NO_POSITION) { + recycler.layoutManager.scrollToPosition(oldPosition) + } + this.recycler = recycler + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.catalogue_list, menu) + + // Initialize search menu + menu.findItem(R.id.action_search).apply { + val searchView = actionView as SearchView + + if (!query.isBlank()) { + expandActionView() + searchView.setQuery(query, true) + searchView.clearFocus() + } + + val searchEventsObservable = searchView.queryTextChangeEvents() + .skip(1) + .share() + val writingObservable = searchEventsObservable + .filter { !it.isSubmitted } + .debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) + val submitObservable = searchEventsObservable + .filter { it.isSubmitted } + + searchViewSubscription?.unsubscribe() + searchViewSubscription = Observable.merge(writingObservable, submitObservable) + .map { it.queryText().toString() } + .distinctUntilChanged() + .subscribeUntilDestroy { searchWithQuery(it) } + + untilDestroySubscriptions.add( + Subscriptions.create { if (isActionViewExpanded) collapseActionView() }) + } + + // Setup filters button + menu.findItem(R.id.action_set_filter).apply { + icon.mutate() + if (presenter.sourceFilters.isEmpty()) { + isEnabled = false + icon.alpha = 128 + } else { + isEnabled = true + icon.alpha = 255 + } + } + + // Show next display mode + menu.findItem(R.id.action_display_mode).apply { + val icon = if (presenter.isListMode) + R.drawable.ic_view_module_white_24dp + else + R.drawable.ic_view_list_white_24dp + setIcon(icon) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_display_mode -> swapDisplayMode() + R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Restarts the request with a new query. + * + * @param newQuery the new query. + */ + private fun searchWithQuery(newQuery: String) { + // If text didn't change, do nothing + if (query == newQuery) + return + + showProgressBar() + adapter?.clear() + + presenter.restartPager(newQuery) + } + + /** + * Called from the presenter when the network request is received. + * + * @param page the current page. + * @param mangas the list of manga of the page. + */ + fun onAddPage(page: Int, mangas: List) { + val adapter = adapter ?: return + hideProgressBar() + if (page == 1) { + adapter.clear() + resetProgressItem() + } + adapter.onLoadMoreComplete(mangas) + } + + /** + * Called from the presenter when the network request fails. + * + * @param error the error received. + */ + fun onAddPageError(error: Throwable) { + Timber.e(error) + val adapter = adapter ?: return + adapter.onLoadMoreComplete(null) + hideProgressBar() + + val message = if (error is NoResultsException) "No results found" else (error.message ?: "") + + snack?.dismiss() + snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.action_retry) { + // If not the first page, show bottom progress bar. + if (adapter.mainItemCount > 0) { + val item = progressItem ?: return@setAction + adapter.addScrollableFooterWithDelay(item, 0, true) + } else { + showProgressBar() + } + presenter.requestNext() + } + } + } + + /** + * Sets a new progress item and reenables the scroll listener. + */ + private fun resetProgressItem() { + progressItem = ProgressItem() + adapter?.endlessTargetCount = 0 + adapter?.setEndlessScrollListener(this, progressItem!!) + } + + /** + * Called by the adapter when scrolled near the bottom. + */ + override fun onLoadMore(lastPosition: Int, currentPage: Int) { + Timber.e("onLoadMore") + if (presenter.hasNextPage()) { + presenter.requestNext() + } else { + adapter?.onLoadMoreComplete(null) + adapter?.endlessTargetCount = 1 + } + } + + override fun noMoreLoad(newItemsSize: Int) { + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the manga initialized + */ + fun onMangaInitialized(manga: Manga) { + getHolder(manga)?.setImage(manga) + } + + /** + * Swaps the current display mode. + */ + fun swapDisplayMode() { + val view = view ?: return + val adapter = adapter ?: return + + presenter.swapDisplayMode() + val isListMode = presenter.isListMode + activity?.invalidateOptionsMenu() + setupRecycler(view) + if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) { + // Initialize mangas if going to grid view or if over wifi when going to list view + val mangas = (0..adapter.itemCount-1).mapNotNull { + (adapter.getItem(it) as? CatalogueItem)?.manga + } + presenter.initializeMangas(mangas) + } + } + + /** + * Returns a preference for the number of manga per row based on the current orientation. + * + * @return the preference. + */ + fun getColumnsPreferenceForCurrentOrientation(): Preference { + return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) + presenter.prefs.portraitColumns() + else + presenter.prefs.landscapeColumns() + } + + /** + * 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): CatalogueHolder? { + val adapter = adapter ?: return null + + adapter.allBoundViewHolders.forEach { holder -> + val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem + if (item != null && item.manga.id!! == manga.id!!) { + return holder as CatalogueHolder + } + } + + return null + } + + /** + * Shows the progress bar. + */ + private fun showProgressBar() { + view?.progress?.visible() + snack?.dismiss() + snack = null + } + + /** + * Hides active progress bars. + */ + private fun hideProgressBar() { + view?.progress?.gone() + } + + /** + * Called when a manga is clicked. + * + * @param position the position of the element clicked. + * @return true if the item should be selected, false otherwise. + */ + override fun onItemClick(position: Int): Boolean { + val item = adapter?.getItem(position) as? CatalogueItem ?: return false + router.pushController(RouterTransaction.with(MangaController(item.manga, true)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + + return false + } + + /** + * Called when a manga is long clicked. + * + * Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga + * in, the list consists of the default category plus the user's categories. The default category is preselected on + * new manga, and on already favorited manga the manga's categories are preselected. + * + * @param position the position of the element clicked. + */ + override fun onItemLongClick(position: Int) { + val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return + if (manga.favorite) { + MaterialDialog.Builder(activity!!) + .items(resources?.getString(R.string.remove_from_library)) + .itemsCallback { _, _, which, _ -> + when (which) { + 0 -> { + presenter.changeMangaFavorite(manga) + adapter?.notifyItemChanged(position) + } + } + }.show() + } else { + presenter.changeMangaFavorite(manga) + adapter?.notifyItemChanged(position) + + 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() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + + } + + /** + * Update manga to use selected categories. + * + * @param mangas The list of manga to move to categories. + * @param categories The list of categories where manga will be placed. + */ + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.updateMangaCategories(manga, categories) + } + +} 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 08ce9336b..4cd6554fa 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 @@ -6,7 +6,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.widget.StateImageViewTarget -import kotlinx.android.synthetic.main.item_catalogue_grid.view.* +import kotlinx.android.synthetic.main.catalogue_grid_item.view.* /** * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. 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 5aa1eecd1..b6207b8a1 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 @@ -3,36 +3,45 @@ package eu.kanade.tachiyomi.ui.catalogue import android.view.Gravity import android.view.LayoutInflater import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import kotlinx.android.synthetic.main.item_catalogue_grid.view.* +import kotlinx.android.synthetic.main.catalogue_grid_item.view.* class CatalogueItem(val manga: Manga) : AbstractFlexibleItem() { override fun getLayoutRes(): Int { - return R.layout.item_catalogue_grid + return R.layout.catalogue_grid_item } - override fun createViewHolder(adapter: FlexibleAdapter>, inflater: LayoutInflater, parent: ViewGroup): CatalogueHolder { + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): CatalogueHolder { + if (parent is AutofitRecyclerView) { - val view = parent.inflate(R.layout.item_catalogue_grid).apply { - card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4) - gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM) + val view = parent.inflate(R.layout.catalogue_grid_item).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) } else { - val view = parent.inflate(R.layout.item_catalogue_list) + val view = parent.inflate(R.layout.catalogue_list_item) return CatalogueListHolder(view, adapter) } } - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: CatalogueHolder, position: Int, payloads: List?) { + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: CatalogueHolder, + position: Int, + payloads: List?) { + holder.onSetValues(manga) } 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 9f98786b0..5b782b167 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 @@ -6,7 +6,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.util.getResourceColor -import kotlinx.android.synthetic.main.item_catalogue_list.view.* +import jp.wasabeef.glide.transformations.CropCircleTransformation +import kotlinx.android.synthetic.main.catalogue_list_item.view.* /** * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. @@ -42,6 +43,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) : .load(manga) .diskCacheStrategy(DiskCacheStrategy.SOURCE) .centerCrop() + .bitmapTransform(CropCircleTransformation(view.context)) .dontAnimate() .skipMemoryCache(true) .placeholder(android.R.color.transparent) 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 daa6d5886..69fd34db7 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 @@ -11,6 +11,7 @@ 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 @@ -25,32 +26,18 @@ import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import rx.subjects.PublishSubject import timber.log.Timber -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** - * Presenter of [CatalogueFragment]. + * Presenter of [CatalogueController]. */ -open class CataloguePresenter : BasePresenter() { - - /** - * Source manager. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Database. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Preferences. - */ - val prefs: PreferencesHelper by injectLazy() - - /** - * Cover cache. - */ - val coverCache: CoverCache by injectLazy() +open class CataloguePresenter( + val sourceManager: SourceManager = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), + val prefs: PreferencesHelper = Injekt.get(), + val coverCache: CoverCache = Injekt.get() +) : BasePresenter() { /** * Enabled sources. @@ -182,7 +169,7 @@ open class CataloguePresenter : BasePresenter() { pageSubscription = Observable.defer { pager.requestNext() } .subscribeFirst({ view, page -> // Nothing to do when onNext is emitted. - }, CatalogueFragment::onAddPageError) + }, CatalogueController::onAddPageError) } /** @@ -317,15 +304,11 @@ open class CataloguePresenter : BasePresenter() { val languages = prefs.enabledLanguages().getOrDefault() val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() - // Ensure at least one language - if (languages.isEmpty()) { - languages.add("en") - } - return sourceManager.getCatalogueSources() .filter { it.lang in languages } .filterNot { it.id.toString() in hiddenCatalogues } - .sortedBy { "(${it.lang}) ${it.name}" } + .sortedBy { "(${it.lang}) ${it.name}" } + + sourceManager.get(LocalSource.ID) as LocalSource } /** @@ -404,7 +387,7 @@ open class CataloguePresenter : BasePresenter() { * @return List of categories, default plus user categories */ fun getCategories(): List { - return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking() + return db.getCategories().executeAsBlocking() } /** @@ -415,10 +398,7 @@ open class CataloguePresenter : BasePresenter() { */ fun getMangaCategoryIds(manga: Manga): Array { val categories = db.getCategoriesForManga(manga).executeAsBlocking() - if (categories.isEmpty()) { - return arrayListOf(Category.createDefault().id).toTypedArray() - } - return categories.map { it.id }.toTypedArray() + return categories.mapNotNull { it.id }.toTypedArray() } /** @@ -427,10 +407,9 @@ open class CataloguePresenter : BasePresenter() { * @param categories the selected categories. * @param manga the manga to move. */ - fun moveMangaToCategories(categories: List, manga: Manga) { - val mc = categories.map { MangaCategory.create(manga, it) } - - db.setMangaCategories(mc, arrayListOf(manga)) + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) } /** @@ -439,8 +418,8 @@ open class CataloguePresenter : BasePresenter() { * @param category the selected category. * @param manga the manga to move. */ - fun moveMangaToCategory(category: Category, manga: Manga) { - moveMangaToCategories(arrayListOf(category), manga) + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) } /** @@ -454,7 +433,7 @@ open class CataloguePresenter : BasePresenter() { if (!manga.favorite) changeMangaFavorite(manga) - moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga) + moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 }) } else { changeMangaFavorite(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 7d1d8cfbe..1f1d75b72 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 @@ -17,7 +17,7 @@ class ProgressItem : AbstractFlexibleItem() { var loadMore = true override fun getLayoutRes(): Int { - return R.layout.progress_item + return R.layout.catalogue_progress_item } override fun createViewHolder(adapter: FlexibleAdapter>, inflater: LayoutInflater, parent: ViewGroup): Holder { 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 9b153cdc1..ad840475e 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 @@ -30,7 +30,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem(holder.itemView.context, android.R.layout.simple_spinner_item, filter.values).apply { - setDropDownViewResource(R.layout.spinner_item) + setDropDownViewResource(R.layout.common_spinner_item) } spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position -> filter.state = position diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt deleted file mode 100755 index a8cceabe5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt +++ /dev/null @@ -1,265 +0,0 @@ -package eu.kanade.tachiyomi.ui.category - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.support.v7.view.ActionMode -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.view.Menu -import android.view.MenuItem -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -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.activity.BaseRxActivity -import kotlinx.android.synthetic.main.activity_edit_categories.* -import kotlinx.android.synthetic.main.toolbar.* -import nucleus.factory.RequiresPresenter - - -/** - * Activity that shows categories. - * Uses R.layout.activity_edit_categories. - * UI related actions should be called from here. - */ -@RequiresPresenter(CategoryPresenter::class) -class CategoryActivity : - BaseRxActivity(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - UndoHelper.OnUndoListener { - - /** - * Object used to show actionMode toolbar. - */ - var actionMode: ActionMode? = null - - /** - * Adapter containing category items. - */ - private lateinit var adapter: CategoryAdapter - - companion object { - /** - * Create new CategoryActivity intent. - * - * @param context context information. - */ - fun newIntent(context: Context): Intent { - return Intent(context, CategoryActivity::class.java) - } - } - - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) - - // Inflate activity_edit_categories.xml. - setContentView(R.layout.activity_edit_categories) - - // Setup the toolbar. - setupToolbar(toolbar) - - // Get new adapter. - adapter = CategoryAdapter(this) - - // Create view and inject category items into view - recycler.layoutManager = LinearLayoutManager(this) - recycler.setHasFixedSize(true) - recycler.adapter = adapter - - adapter.isHandleDragEnabled = true - - // Create OnClickListener for creating new category - fab.setOnClickListener { - MaterialDialog.Builder(this) - .title(R.string.action_add_category) - .negativeText(android.R.string.cancel) - .input(R.string.name, 0, false) - { dialog, input -> presenter.createCategory(input.toString()) } - .show() - } - } - - /** - * Fill adapter with category items - * - * @param categories list containing categories - */ - fun setCategories(categories: List) { - actionMode?.finish() - adapter.updateDataSet(categories.toMutableList()) - val selected = categories.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(categories.indexOf(it)) } - } - } - - /** - * Show MaterialDialog which let user change category name. - * - * @param category category that will be edited. - */ - private fun editCategory(category: Category) { - MaterialDialog.Builder(this) - .title(R.string.action_rename_category) - .negativeText(android.R.string.cancel) - .input(getString(R.string.name), category.name, false) - { dialog, input -> presenter.renameCategory(category, input.toString()) } - .show() - } - - /** - * Called when action mode item clicked. - * - * @param actionMode action mode toolbar. - * @param menuItem selected menu item. - * - * @return action mode item clicked exist status - */ - override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean { - when (menuItem.itemId) { - R.id.action_delete -> { - UndoHelper(adapter, this) - .withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener { - override fun onPreAction(): Boolean { - adapter.selectedPositions.forEach { adapter.getItem(it).isSelected = false } - return false - } - - override fun onPostAction() { - actionMode.finish() - } - }) - .remove(adapter.selectedPositions, recycler.parent as View, - R.string.snack_categories_deleted, R.string.action_undo, 3000) - } - R.id.action_edit -> { - // Edit selected category - if (adapter.selectedItemCount == 1) { - val position = adapter.selectedPositions.first() - editCategory(adapter.getItem(position).category) - } - } - else -> return false - } - return true - } - - /** - * Inflate menu when action mode selected. - * - * @param mode ActionMode object - * @param menu Menu object - * - * @return true - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate menu. - mode.menuInflater.inflate(R.menu.category_selection, menu) - // Enable adapter multi selection. - adapter.mode = FlexibleAdapter.MODE_MULTI - return true - } - - /** - * Called each time the action mode is shown. - * Always called after onCreateActionMode - * - * @return false - */ - override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean { - val count = adapter.selectedItemCount - actionMode.title = getString(R.string.label_selected, count) - - // Show edit button only when one item is selected - val editItem = actionMode.menu.findItem(R.id.action_edit) - editItem.isVisible = count == 1 - return true - } - - /** - * Called when action mode destroyed. - * - * @param mode ActionMode object. - */ - override fun onDestroyActionMode(mode: ActionMode?) { - // Reset adapter to single selection - adapter.mode = FlexibleAdapter.MODE_IDLE - adapter.clearSelection() - actionMode = null - } - - /** - * Called when item in list is clicked. - * - * @param position position of clicked item. - */ - override fun onItemClick(position: Int): Boolean { - // Check if action mode is initialized and selected item exist. - if (actionMode != null && position != RecyclerView.NO_POSITION) { - toggleSelection(position) - return true - } else { - return false - } - } - - /** - * Called when item long clicked - * - * @param position position of clicked item. - */ - override fun onItemLongClick(position: Int) { - // Check if action mode is initialized. - if (actionMode == null) { - // Initialize action mode - actionMode = startSupportActionMode(this) - } - - // Set item as selected - toggleSelection(position) - } - - /** - * Toggle the selection state of an item. - * If the item was the last one in the selection and is unselected, the ActionMode is finished. - */ - private fun toggleSelection(position: Int) { - //Mark the position selected - adapter.toggleSelection(position) - - if (adapter.selectedItemCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - } - } - - /** - * Called when an item is released from a drag. - */ - fun onItemReleased() { - val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category } - presenter.reorderCategories(categories) - } - - /** - * Called when the undo action is clicked in the snackbar. - */ - override fun onUndoConfirmed(action: Int) { - adapter.restoreDeletedItems() - } - - /** - * Called when the time to restore the items expires. - */ - override fun onDeleteConfirmed(action: Int) { - presenter.deleteCategories(adapter.deletedItems.map { it.category }) - } - -} \ 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 5f3b89fee..907a5a04a 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 @@ -3,31 +3,48 @@ package eu.kanade.tachiyomi.ui.category import eu.davidea.flexibleadapter.FlexibleAdapter /** - * Adapter of CategoryHolder. - * Connection between Activity and Holder - * Holder updates should be called from here. + * Custom adapter for categories. * - * @param activity activity that created adapter - * @constructor Creates a CategoryAdapter object + * @param controller The containing controller. */ -class CategoryAdapter(private val activity: CategoryActivity) : - FlexibleAdapter(null, activity, true) { +class CategoryAdapter(controller: CategoryController) : + FlexibleAdapter(null, controller, true) { /** - * Called when item is released. + * Listener called when an item of the list is released. */ - fun onItemReleased() { - activity.onItemReleased() - } + val onItemReleaseListener: OnItemReleaseListener = controller + /** + * Clears the active selections from the list and the model. + */ override fun clearSelection() { super.clearSelection() - (0..itemCount-1).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 } + } + + /** + * Toggles the selection of the given position. + * + * @param position The position to toggle. + */ override fun toggleSelection(position: Int) { super.toggleSelection(position) getItem(position).isSelected = isSelected(position) } + interface OnItemReleaseListener { + /** + * Called when an item of the list is released. + */ + fun onItemReleased(position: Int) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt new file mode 100644 index 000000000..81243c50d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt @@ -0,0 +1,321 @@ +package eu.kanade.tachiyomi.ui.category + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.view.ActionMode +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.* +import com.jakewharton.rxbinding.view.clicks +import eu.davidea.flexibleadapter.FlexibleAdapter +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.* + +/** + * Controller to manage the categories for the users' library. + */ +class CategoryController : NucleusController(), + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + CategoryAdapter.OnItemReleaseListener, + CategoryCreateDialog.Listener, + CategoryRenameDialog.Listener, + UndoHelper.OnUndoListener { + + /** + * Object used to show ActionMode toolbar. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing category items. + */ + private var adapter: CategoryAdapter? = null + + /** + * Undo helper for deleting categories. + */ + private var undoHelper: UndoHelper? = null + + /** + * Creates the presenter for this controller. Not to be manually called. + */ + override fun createPresenter() = CategoryPresenter() + + /** + * Returns the toolbar title to show when this controller is attached. + */ + override fun getTitle(): String? { + return resources?.getString(R.string.action_edit_categories) + } + + /** + * Returns the view of this controller. + * + * @param inflater The layout inflater to create the view from XML. + * @param container The parent view for this one. + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.categories_controller, container, false) + } + + /** + * Called after view inflation. Used to initialize the view. + * + * @param view The view of this controller. + * @param savedViewState The saved state of the view. + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + adapter = CategoryAdapter(this@CategoryController) + recycler.layoutManager = LinearLayoutManager(context) + recycler.setHasFixedSize(true) + recycler.adapter = adapter + adapter?.isHandleDragEnabled = true + + fab.clicks().subscribeUntilDestroy { + CategoryCreateDialog(this@CategoryController).showDialog(router, null) + } + } + } + + /** + * Called when the view is being destroyed. Used to release references and remove callbacks. + * + * @param view The view of this controller. + */ + override fun onDestroyView(view: View) { + super.onDestroyView(view) + undoHelper?.dismissNow() // confirm categories deletion if required + undoHelper = null + actionMode = null + adapter = null + } + + /** + * Called from the presenter when the categories are updated. + * + * @param categories The new list of categories to display. + */ + fun setCategories(categories: List) { + actionMode?.finish() + adapter?.updateDataSet(categories.toMutableList()) + val selected = categories.filter { it.isSelected } + if (selected.isNotEmpty()) { + selected.forEach { onItemLongClick(categories.indexOf(it)) } + } + } + + /** + * Called when action mode is first created. The menu supplied will be used to generate action + * buttons for the action mode. + * + * @param mode ActionMode being created. + * @param menu Menu used to populate action buttons. + * @return true if the action mode should be created, false if entering this mode should be + * aborted. + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate menu. + mode.menuInflater.inflate(R.menu.category_selection, menu) + // Enable adapter multi selection. + adapter?.mode = FlexibleAdapter.MODE_MULTI + return true + } + + /** + * Called to refresh an action mode's action menu whenever it is invalidated. + * + * @param mode ActionMode being prepared. + * @param menu Menu used to populate action buttons. + * @return true if the menu or action mode was updated, false otherwise. + */ + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val adapter = adapter ?: return false + val count = adapter.selectedItemCount + mode.title = resources?.getString(R.string.label_selected, count) + + // Show edit button only when one item is selected + val editItem = mode.menu.findItem(R.id.action_edit) + editItem.isVisible = count == 1 + return true + } + + /** + * Called to report a user click on an action button. + * + * @param mode The current ActionMode. + * @param item The item that was clicked. + * @return true if this callback handled the event, false if the standard MenuItem invocation + * should continue. + */ + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val adapter = adapter ?: return false + + when (item.itemId) { + R.id.action_delete -> { + undoHelper = UndoHelper(adapter, this).apply { + withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener { + override fun onPreAction(): Boolean { + adapter.clearModelSelection() + return false + } + + override fun onPostAction() { + mode.finish() + } + }) + remove(adapter.selectedPositions, view!!, + R.string.snack_categories_deleted, R.string.action_undo, 3000) + } + } + R.id.action_edit -> { + // Edit selected category + if (adapter.selectedItemCount == 1) { + val position = adapter.selectedPositions.first() + editCategory(adapter.getItem(position).category) + } + } + else -> return false + } + return true + } + + /** + * Called when an action mode is about to be exited and destroyed. + * + * @param mode The current ActionMode being destroyed. + */ + override fun onDestroyActionMode(mode: ActionMode) { + // Reset adapter to single selection + adapter?.mode = FlexibleAdapter.MODE_IDLE + adapter?.clearSelection() + actionMode = null + } + + /** + * Called when an item in the list is clicked. + * + * @param position The position of the clicked item. + * @return true if this click should enable selection mode. + */ + override fun onItemClick(position: Int): Boolean { + // Check if action mode is initialized and selected item exist. + if (actionMode != null && position != RecyclerView.NO_POSITION) { + toggleSelection(position) + return true + } else { + return false + } + } + + /** + * Called when an item in the list is long clicked. + * + * @param position The position of the clicked item. + */ + override fun onItemLongClick(position: Int) { + val activity = activity as? AppCompatActivity ?: return + + // Check if action mode is initialized. + if (actionMode == null) { + // Initialize action mode + actionMode = activity.startSupportActionMode(this) + } + + // Set item as selected + toggleSelection(position) + } + + /** + * Toggle the selection state of an item. + * If the item was the last one in the selection and is unselected, the ActionMode is finished. + * + * @param position The position of the item to toggle. + */ + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + + //Mark the position selected + adapter.toggleSelection(position) + + if (adapter.selectedItemCount == 0) { + actionMode?.finish() + } else { + actionMode?.invalidate() + } + } + + /** + * Called when an item is released from a drag. + * + * @param position The position of the released item. + */ + override fun onItemReleased(position: Int) { + val adapter = adapter ?: return + val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category } + presenter.reorderCategories(categories) + } + + /** + * Called when the undo action is clicked in the snackbar. + * + * @param action The action performed. + */ + override fun onUndoConfirmed(action: Int) { + adapter?.restoreDeletedItems() + } + + /** + * Called when the time to restore the items expires. + * + * @param action The action performed. + */ + override fun onDeleteConfirmed(action: Int) { + val adapter = adapter ?: return + presenter.deleteCategories(adapter.deletedItems.map { it.category }) + } + + /** + * Show a dialog to let the user change the category name. + * + * @param category The category to be edited. + */ + private fun editCategory(category: Category) { + CategoryRenameDialog(this, category).showDialog(router) + } + + /** + * Renames the given category with the given name. + * + * @param category The category to rename. + * @param name The new name of the category. + */ + override fun renameCategory(category: Category, name: String) { + presenter.renameCategory(category, name) + } + + /** + * Creates a new category with the given name. + * + * @param name The name of the new category. + */ + override fun createCategory(name: String) { + presenter.createCategory(name) + } + + /** + * Called from the presenter when a category with the given name already exists. + */ + fun onCategoryExistsError() { + activity?.toast(R.string.error_category_exists) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt new file mode 100644 index 000000000..dfa4bad32 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.ui.category + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +/** + * Dialog to create a new category for the library. + */ +class CategoryCreateDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : CategoryCreateDialog.Listener { + + /** + * Name of the new category. Value updated with each input from the user. + */ + private var currentName = "" + + constructor(target: T) : this() { + targetController = target + } + + /** + * Called when creating the dialog for this controller. + * + * @param savedViewState The saved state of this dialog. + * @return a new dialog instance. + */ + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.action_add_category) + .negativeText(android.R.string.cancel) + .alwaysCallInputCallback() + .input(resources?.getString(R.string.name), currentName, false, { _, input -> + currentName = input.toString() + }) + .onPositive { _, _ -> (targetController as? Listener)?.createCategory(currentName) } + .build() + } + + interface Listener { + fun createCategory(name: String) + } + +} \ No newline at end of file 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 35f58a7b5..906bbb910 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,17 +7,13 @@ 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 kotlinx.android.synthetic.main.item_edit_categories.view.* +import kotlinx.android.synthetic.main.categories_item.view.* /** - * Holder that contains category item. - * Uses R.layout.item_edit_categories. - * UI related actions should be called from here. + * Holder used to display category items. * - * @param view view of category item. - * @param adapter adapter belonging to holder. - * - * @constructor Create CategoryHolder object + * @param view The view used by category items. + * @param adapter The adapter containing this holder. */ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) { @@ -32,9 +28,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol } /** - * Update category item values. + * Binds this holder with the given category. * - * @param category category of item. + * @param category The category to bind. */ fun bind(category: Category) { // Set capitalized title. @@ -47,9 +43,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol } /** - * Returns circle letter image + * Returns circle letter image. * - * @param text first letter of string + * @param text The first letter of string. */ private fun getRound(text: String): TextDrawable { val size = Math.min(itemView.image.width, itemView.image.height) @@ -63,9 +59,14 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol .buildRound(text, ColorGenerator.MATERIAL.getColor(text)) } + /** + * Called when an item is released. + * + * @param position The position of the released item. + */ override fun onItemReleased(position: Int) { super.onItemReleased(position) - adapter.onItemReleased() + adapter.onItemReleaseListener.onItemReleased(position) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt index 3dd3ad5a7..40c52cd16 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 @@ -8,29 +8,62 @@ 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. + */ class CategoryItem(val category: Category) : AbstractFlexibleItem() { + /** + * Whether this item is currently selected. + */ var isSelected = false + /** + * Returns the layout resource for this item. + */ override fun getLayoutRes(): Int { - return R.layout.item_edit_categories + return R.layout.categories_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, + /** + * Returns a new view holder for this item. + * + * @param adapter The adapter of this item. + * @param inflater The layout inflater for XML inflation. + * @param parent The container view. + */ + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, parent: ViewGroup): CategoryHolder { + return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter) } - override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CategoryHolder, - position: Int, payloads: List?) { + /** + * Binds the given view holder with this item. + * + * @param adapter The adapter of this item. + * @param holder The holder to bind. + * @param position The position of this item in the adapter. + * @param payloads List of partial changes. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: CategoryHolder, + position: Int, + payloads: List?) { + holder.bind(category) } + /** + * Returns true if this item is draggable. + */ override fun isDraggable(): Boolean { return true } override fun equals(other: Any?): Boolean { + if (this === other) return true if (other is CategoryItem) { return category.id == other.category.id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt index b8691aa5d..64c7af09a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt @@ -1,31 +1,31 @@ package eu.kanade.tachiyomi.ui.category import android.os.Bundle -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.toast +import rx.Observable import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** - * Presenter of CategoryActivity. - * Contains information and data for activity. - * Observable updates should be called from here. + * Presenter of [CategoryController]. Used to manage the categories of the library. */ -class CategoryPresenter : BasePresenter() { - - /** - * Used to connect to database. - */ - private val db: DatabaseHelper by injectLazy() +class CategoryPresenter( + private val db: DatabaseHelper = Injekt.get() +) : BasePresenter() { /** * List containing categories. */ private var categories: List = emptyList() + /** + * Called when the presenter is created. + * + * @param savedState The saved state of this presenter. + */ override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -33,18 +33,18 @@ class CategoryPresenter : BasePresenter() { .doOnNext { categories = it } .map { it.map(::CategoryItem) } .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(CategoryActivity::setCategories) + .subscribeLatestCache(CategoryController::setCategories) } /** - * Create category and add it to database + * Creates and adds a new category to the database. * - * @param name name of category + * @param name The name of the category to create. */ fun createCategory(name: String) { // Do not allow duplicate categories. - if (categories.any { it.name.equals(name, true) }) { - context.toast(R.string.error_category_exists) + if (categoryExists(name)) { + Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() }) return } @@ -59,18 +59,18 @@ class CategoryPresenter : BasePresenter() { } /** - * Delete category from database + * Deletes the given categories from the database. * - * @param categories list of categories + * @param categories The list of categories to delete. */ fun deleteCategories(categories: List) { db.deleteCategories(categories).asRxObservable().subscribe() } /** - * Reorder categories in database + * Reorders the given categories in the database. * - * @param categories list of categories + * @param categories The list of categories to reorder. */ fun reorderCategories(categories: List) { categories.forEachIndexed { i, category -> @@ -81,19 +81,27 @@ class CategoryPresenter : BasePresenter() { } /** - * Rename a category + * Renames a category. * - * @param category category that gets renamed - * @param name new name of category + * @param category The category to rename. + * @param name The new name of the category. */ fun renameCategory(category: Category, name: String) { // Do not allow duplicate categories. - if (categories.any { it.name.equals(name, true) }) { - context.toast(R.string.error_category_exists) + if (categoryExists(name)) { + Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() }) return } category.name = name db.insertCategory(category).asRxObservable().subscribe() } + + /** + * Returns true if a category with the given name already exists. + */ + fun categoryExists(name: String): Boolean { + return categories.any { it.name.equals(name, true) } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt new file mode 100644 index 000000000..286093b06 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt @@ -0,0 +1,86 @@ +package eu.kanade.tachiyomi.ui.category + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +/** + * Dialog to rename an existing category of the library. + */ +class CategoryRenameDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : CategoryRenameDialog.Listener { + + private var category: Category? = null + + /** + * Name of the new category. Value updated with each input from the user. + */ + private var currentName = "" + + constructor(target: T, category: Category) : this() { + targetController = target + this.category = category + currentName = category.name + } + + /** + * Called when creating the dialog for this controller. + * + * @param savedViewState The saved state of this dialog. + * @return a new dialog instance. + */ + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.action_rename_category) + .negativeText(android.R.string.cancel) + .alwaysCallInputCallback() + .input(resources!!.getString(R.string.name), currentName, false, { _, input -> + currentName = input.toString() + }) + .onPositive { _, _ -> onPositive() } + .build() + } + + /** + * Called to save this Controller's state in the event that its host Activity is destroyed. + * + * @param outState The Bundle into which data should be saved + */ + override fun onSaveInstanceState(outState: Bundle) { + outState.putSerializable(CATEGORY_KEY, category) + super.onSaveInstanceState(outState) + } + + /** + * Restores data that was saved in the [onSaveInstanceState] method. + * + * @param savedInstanceState The bundle that has data to be restored + */ + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + category = savedInstanceState.getSerializable(CATEGORY_KEY) as? Category + } + + /** + * Called when the positive button of the dialog is clicked. + */ + private fun onPositive() { + val target = targetController as? Listener ?: return + val category = category ?: return + + target.renameCategory(category, currentName) + } + + interface Listener { + fun renameCategory(category: Category, name: String) + } + + private companion object { + const val CATEGORY_KEY = "CategoryRenameDialog.category" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt index 5f5e7d361..795b85f13 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt @@ -1,8 +1,7 @@ package eu.kanade.tachiyomi.ui.download -import android.content.Context +import android.support.v7.widget.RecyclerView import android.view.ViewGroup -import eu.davidea.flexibleadapter4.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.util.inflate @@ -12,7 +11,9 @@ import eu.kanade.tachiyomi.util.inflate * * @param context the context of the fragment containing this adapter. */ -class DownloadAdapter(private val context: Context) : FlexibleAdapter() { +class DownloadAdapter : RecyclerView.Adapter() { + + private var items = emptyList() init { setHasStableIds(true) @@ -24,10 +25,17 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter) { - mItems = downloads + items = downloads notifyDataSetChanged() } + /** + * Returns the number of downloads in the adapter + */ + override fun getItemCount(): Int { + return items.size + } + /** * Returns the identifier for a download. * @@ -35,7 +43,7 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter() { - /** - * Adapter containing the active downloads. - */ - private lateinit var adapter: DownloadAdapter - - /** - * Subscription list to be cleared during [onDestroy]. - */ - private val subscriptions by lazy { CompositeSubscription() } - - /** - * Map of subscriptions for active downloads. - */ - private val progressSubscriptions by lazy { HashMap() } - - /** - * Whether the download queue is running or not. - */ - private var isRunning: Boolean = false - - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) - setContentView(R.layout.activity_download_manager) - setupToolbar(toolbar) - setToolbarTitle(R.string.label_download_queue) - - // Check if download queue is empty and update information accordingly. - setInformationView() - - // Initialize adapter. - adapter = DownloadAdapter(this) - recycler.adapter = adapter - - // Set the layout manager for the recycler and fixed size. - recycler.layoutManager = LinearLayoutManager(this) - recycler.setHasFixedSize(true) - - // Suscribe to changes - subscriptions += DownloadService.runningRelay - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { onQueueStatusChange(it) } - - subscriptions += presenter.getDownloadStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { onStatusChange(it) } - - subscriptions += presenter.getDownloadProgressObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { onUpdateDownloadedPages(it) } - } - - override fun onDestroy() { - for (subscription in progressSubscriptions.values) { - subscription.unsubscribe() - } - progressSubscriptions.clear() - subscriptions.clear() - super.onDestroy() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.download_queue, menu) - return true - } - - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - // Set start button visibility. - menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() - - // Set pause button visibility. - menu.findItem(R.id.pause_queue).isVisible = isRunning - - // Set clear button visibility. - menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.start_queue -> DownloadService.start(this) - R.id.pause_queue -> { - DownloadService.stop(this) - presenter.pauseDownloads() - } - R.id.clear_queue -> { - DownloadService.stop(this) - presenter.clearQueue() - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Called when the status of a download changes. - * - * @param download the download whose status has changed. - */ - private fun onStatusChange(download: Download) { - when (download.status) { - Download.DOWNLOADING -> { - observeProgress(download) - // Initial update of the downloaded pages - onUpdateDownloadedPages(download) - } - Download.DOWNLOADED -> { - unsubscribeProgress(download) - onUpdateProgress(download) - onUpdateDownloadedPages(download) - } - Download.ERROR -> unsubscribeProgress(download) - } - } - - /** - * Observe the progress of a download and notify the view. - * - * @param download the download to observe its progress. - */ - private fun observeProgress(download: Download) { - val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) - // Get the sum of percentages for all the pages. - .flatMap { - Observable.from(download.pages) - .map(Page::progress) - .reduce { x, y -> x + y } - } - // Keep only the latest emission to avoid backpressure. - .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { progress -> - // Update the view only if the progress has changed. - if (download.totalProgress != progress) { - download.totalProgress = progress - onUpdateProgress(download) - } - } - - // Avoid leaking subscriptions - progressSubscriptions.remove(download)?.unsubscribe() - - progressSubscriptions.put(download, subscription) - } - - /** - * Unsubscribes the given download from the progress subscriptions. - * - * @param download the download to unsubscribe. - */ - private fun unsubscribeProgress(download: Download) { - progressSubscriptions.remove(download)?.unsubscribe() - } - - /** - * Called when the queue's status has changed. Updates the visibility of the buttons. - * - * @param running whether the queue is now running or not. - */ - private fun onQueueStatusChange(running: Boolean) { - isRunning = running - supportInvalidateOptionsMenu() - - // Check if download queue is empty and update information accordingly. - setInformationView() - } - - /** - * Called from the presenter to assign the downloads for the adapter. - * - * @param downloads the downloads from the queue. - */ - fun onNextDownloads(downloads: List) { - supportInvalidateOptionsMenu() - setInformationView() - adapter.setItems(downloads) - } - - /** - * Called when the progress of a download changes. - * - * @param download the download whose progress has changed. - */ - fun onUpdateProgress(download: Download) { - getHolder(download)?.notifyProgress() - } - - /** - * Called when a page of a download is downloaded. - * - * @param download the download whose page has been downloaded. - */ - fun onUpdateDownloadedPages(download: Download) { - getHolder(download)?.notifyDownloadedPages() - } - - /** - * Returns the holder for the given download. - * - * @param download the download to find. - * @return the holder of the download or null if it's not bound. - */ - private fun getHolder(download: Download): DownloadHolder? { - return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder - } - - /** - * Set information view when queue is empty - */ - private fun setInformationView() { - updateEmptyView(presenter.downloadQueue.isEmpty(), - R.string.information_no_downloads, R.drawable.ic_file_download_black_128dp) - } - - fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) { - if (show) empty_view.show(drawable, textResource) else empty_view.hide() - } -} +package eu.kanade.tachiyomi.ui.download + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.view.* +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import kotlinx.android.synthetic.main.download_controller.view.* +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * Controller that shows the currently active downloads. + * Uses R.layout.fragment_download_queue. + */ +class DownloadController : NucleusController() { + + /** + * Adapter containing the active downloads. + */ + private var adapter: DownloadAdapter? = null + + /** + * Map of subscriptions for active downloads. + */ + private val progressSubscriptions by lazy { HashMap() } + + /** + * Whether the download queue is running or not. + */ + private var isRunning: Boolean = false + + init { + setHasOptionsMenu(true) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.download_controller, container, false) + } + + override fun createPresenter(): DownloadPresenter { + return DownloadPresenter() + } + + override fun getTitle(): String? { + return resources?.getString(R.string.label_download_queue) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + // Check if download queue is empty and update information accordingly. + setInformationView() + + // Initialize adapter. + adapter = DownloadAdapter() + with(view) { + recycler.adapter = adapter + + // Set the layout manager for the recycler and fixed size. + recycler.layoutManager = LinearLayoutManager(context) + recycler.setHasFixedSize(true) + } + + // Suscribe to changes + DownloadService.runningRelay + .observeOn(AndroidSchedulers.mainThread()) + .subscribeUntilDestroy { onQueueStatusChange(it) } + + presenter.getDownloadStatusObservable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeUntilDestroy { onStatusChange(it) } + + presenter.getDownloadProgressObservable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeUntilDestroy { onUpdateDownloadedPages(it) } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + for (subscription in progressSubscriptions.values) { + subscription.unsubscribe() + } + progressSubscriptions.clear() + adapter = null + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.download_queue, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // Set start button visibility. + menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() + + // Set pause button visibility. + menu.findItem(R.id.pause_queue).isVisible = isRunning + + // Set clear button visibility. + menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val context = applicationContext ?: return false + when (item.itemId) { + R.id.start_queue -> DownloadService.start(context) + R.id.pause_queue -> { + DownloadService.stop(context) + presenter.pauseDownloads() + } + R.id.clear_queue -> { + DownloadService.stop(context) + presenter.clearQueue() + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Called when the status of a download changes. + * + * @param download the download whose status has changed. + */ + private fun onStatusChange(download: Download) { + when (download.status) { + Download.DOWNLOADING -> { + observeProgress(download) + // Initial update of the downloaded pages + onUpdateDownloadedPages(download) + } + Download.DOWNLOADED -> { + unsubscribeProgress(download) + onUpdateProgress(download) + onUpdateDownloadedPages(download) + } + Download.ERROR -> unsubscribeProgress(download) + } + } + + /** + * Observe the progress of a download and notify the view. + * + * @param download the download to observe its progress. + */ + private fun observeProgress(download: Download) { + val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) + // Get the sum of percentages for all the pages. + .flatMap { + Observable.from(download.pages) + .map(Page::progress) + .reduce { x, y -> x + y } + } + // Keep only the latest emission to avoid backpressure. + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { progress -> + // Update the view only if the progress has changed. + if (download.totalProgress != progress) { + download.totalProgress = progress + onUpdateProgress(download) + } + } + + // Avoid leaking subscriptions + progressSubscriptions.remove(download)?.unsubscribe() + + progressSubscriptions.put(download, subscription) + } + + /** + * Unsubscribes the given download from the progress subscriptions. + * + * @param download the download to unsubscribe. + */ + private fun unsubscribeProgress(download: Download) { + progressSubscriptions.remove(download)?.unsubscribe() + } + + /** + * Called when the queue's status has changed. Updates the visibility of the buttons. + * + * @param running whether the queue is now running or not. + */ + private fun onQueueStatusChange(running: Boolean) { + isRunning = running + activity?.invalidateOptionsMenu() + + // Check if download queue is empty and update information accordingly. + setInformationView() + } + + /** + * Called from the presenter to assign the downloads for the adapter. + * + * @param downloads the downloads from the queue. + */ + fun onNextDownloads(downloads: List) { + activity?.invalidateOptionsMenu() + setInformationView() + adapter?.setItems(downloads) + } + + /** + * Called when the progress of a download changes. + * + * @param download the download whose progress has changed. + */ + fun onUpdateProgress(download: Download) { + getHolder(download)?.notifyProgress() + } + + /** + * Called when a page of a download is downloaded. + * + * @param download the download whose page has been downloaded. + */ + fun onUpdateDownloadedPages(download: Download) { + getHolder(download)?.notifyDownloadedPages() + } + + /** + * Returns the holder for the given download. + * + * @param download the download to find. + * @return the holder of the download or null if it's not bound. + */ + private fun getHolder(download: Download): DownloadHolder? { + val recycler = view?.recycler ?: return null + return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder + } + + /** + * Set information view when queue is empty + */ + private fun setInformationView() { + val emptyView = view?.empty_view ?: return + if (presenter.downloadQueue.isEmpty()) { + emptyView.show(R.drawable.ic_file_download_black_128dp, + R.string.information_no_downloads) + } else { + emptyView.hide() + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt index e11dbe78f..39211c01c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt @@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.download import android.support.v7.widget.RecyclerView import android.view.View import eu.kanade.tachiyomi.data.download.model.Download -import kotlinx.android.synthetic.main.item_download.view.* +import kotlinx.android.synthetic.main.download_item.view.* /** * Class used to hold the data of a download. - * All the elements from the layout file "item_download" are available in this class. + * All the elements from the layout file "download_item" are available in this class. * * @param view the inflated view for this holder. * @constructor creates a new download holder. 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 2664a70e3..4e5b5e288 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 @@ -12,9 +12,9 @@ import uy.kohesive.injekt.injectLazy import java.util.* /** - * Presenter of [DownloadActivity]. + * Presenter of [DownloadController]. */ -class DownloadPresenter : BasePresenter() { +class DownloadPresenter : BasePresenter() { /** * Download manager. @@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter() { downloadQueue.getUpdatedObservable() .observeOn(AndroidSchedulers.mainThread()) .map { ArrayList(it) } - .subscribeLatestCache(DownloadActivity::onNextDownloads, { view, error -> + .subscribeLatestCache(DownloadController::onNextDownloads, { view, 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 new file mode 100644 index 000000000..730b5e991 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.ui.latest_updates + +import android.support.v4.widget.DrawerLayout +import android.view.Menu +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter + +/** + * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. + */ +class LatestUpdatesController : CatalogueController() { + + override fun createPresenter(): CataloguePresenter { + return LatestUpdatesPresenter() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + menu.findItem(R.id.action_search).isVisible = false + menu.findItem(R.id.action_set_filter).isVisible = false + } + + override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { + return null + } + + override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt deleted file mode 100755 index b567e79d1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt +++ /dev/null @@ -1,29 +0,0 @@ -package eu.kanade.tachiyomi.ui.latest_updates - -import android.view.Menu -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment -import nucleus.factory.RequiresPresenter - -/** - * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. - */ -@RequiresPresenter(LatestUpdatesPresenter::class) -class LatestUpdatesFragment : CatalogueFragment() { - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_search).isVisible = false - menu.findItem(R.id.action_set_filter).isVisible = false - - } - - companion object { - - fun newInstance(): LatestUpdatesFragment { - return LatestUpdatesFragment() - } - - } - -} \ 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 bcb27c418..924425b62 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 @@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.Pager /** - * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter. + * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter. */ class LatestUpdatesPresenter : CataloguePresenter() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt new file mode 100644 index 000000000..08f933c8e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.ui.library + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : + DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { + + private var mangas = emptyList() + + private var categories = emptyList() + + private var preselected = emptyArray() + + constructor(target: T, mangas: List, categories: List, + preselected: Array) : this() { + + this.mangas = mangas + this.categories = categories + this.preselected = preselected + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.action_move_category) + .items(categories.map { it.name }) + .itemsCallbackMultiChoice(preselected) { dialog, _, _ -> + val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty() + (targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories) + true + } + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .build() + } + + interface Listener { + fun updateCategoriesForMangas(mangas: List, categories: List) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt new file mode 100644 index 000000000..1aa376eb8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.library + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.widget.DialogCheckboxView + +class DeleteLibraryMangasDialog(bundle: Bundle? = null) : + DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener { + + private var mangas = emptyList() + + constructor(target: T, mangas: List) : this() { + this.mangas = mangas + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val view = DialogCheckboxView(activity!!).apply { + setDescription(R.string.confirm_delete_manga) + setOptionDescription(R.string.also_delete_chapters) + } + + return MaterialDialog.Builder(activity!!) + .title(R.string.action_remove) + .customView(view, true) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + val deleteChapters = view.isChecked() + (targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters) + } + .build() + } + + interface Listener { + fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt index fe4433ead..ae19c08ad 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt @@ -1,88 +1,88 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter - -/** - * This adapter stores the categories from the library, used with a ViewPager. - * - * @constructor creates an instance of the adapter. - */ -class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() { - - /** - * The categories to bind in the adapter. - */ - var categories: List = emptyList() - // This setter helps to not refresh the adapter if the reference to the list doesn't change. - set(value) { - if (field !== value) { - field = value - notifyDataSetChanged() - } - } - - /** - * Creates a new view for this adapter. - * - * @return a new view. - */ - override fun createView(container: ViewGroup): View { - val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView - view.onCreate(fragment) - return view - } - - /** - * Binds a view with a position. - * - * @param view the view to bind. - * @param position the position in the adapter. - */ - override fun bindView(view: View, position: Int) { - (view as LibraryCategoryView).onBind(categories[position]) - } - - /** - * Recycles a view. - * - * @param view the view to recycle. - * @param position the position in the adapter. - */ - override fun recycleView(view: View, position: Int) { - (view as LibraryCategoryView).onRecycle() - } - - /** - * Returns the number of categories. - * - * @return the number of categories or 0 if the list is null. - */ - override fun getCount(): Int { - return categories.size - } - - /** - * Returns the title to display for a category. - * - * @param position the position of the element. - * @return the title to display. - */ - override fun getPageTitle(position: Int): CharSequence { - return categories[position].name - } - - /** - * Returns the position of the view. - */ - override fun getItemPosition(obj: Any?): Int { - val view = obj as? LibraryCategoryView ?: return POSITION_NONE - val index = categories.indexOfFirst { it.id == view.category.id } - return if (index == -1) POSITION_NONE else index - } - +package eu.kanade.tachiyomi.ui.library + +import android.view.View +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter + +/** + * This adapter stores the categories from the library, used with a ViewPager. + * + * @constructor creates an instance of the adapter. + */ +class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() { + + /** + * The categories to bind in the adapter. + */ + var categories: List = emptyList() + // This setter helps to not refresh the adapter if the reference to the list doesn't change. + set(value) { + if (field !== value) { + field = value + notifyDataSetChanged() + } + } + + /** + * Creates a new view for this adapter. + * + * @return a new view. + */ + override fun createView(container: ViewGroup): View { + val view = container.inflate(R.layout.library_category) as LibraryCategoryView + view.onCreate(controller) + return view + } + + /** + * Binds a view with a position. + * + * @param view the view to bind. + * @param position the position in the adapter. + */ + override fun bindView(view: View, position: Int) { + (view as LibraryCategoryView).onBind(categories[position]) + } + + /** + * Recycles a view. + * + * @param view the view to recycle. + * @param position the position in the adapter. + */ + override fun recycleView(view: View, position: Int) { + (view as LibraryCategoryView).onRecycle() + } + + /** + * Returns the number of categories. + * + * @return the number of categories or 0 if the list is null. + */ + override fun getCount(): Int { + return categories.size + } + + /** + * Returns the title to display for a category. + * + * @param position the position of the element. + * @return the title to display. + */ + override fun getPageTitle(position: Int): CharSequence { + return categories[position].name + } + + /** + * Returns the position of the view. + */ + override fun getItemPosition(obj: Any?): Int { + val view = obj as? LibraryCategoryView ?: return POSITION_NONE + val index = categories.indexOfFirst { it.id == view.category.id } + return if (index == -1) POSITION_NONE else index + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 5aad6f4cf..aa5d57961 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -1,39 +1,22 @@ package eu.kanade.tachiyomi.ui.library -import android.os.Handler -import android.os.Looper -import android.view.Gravity -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.widget.FrameLayout -import eu.davidea.flexibleadapter4.FlexibleAdapter -import eu.kanade.tachiyomi.R +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.all.EHentai -import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import exh.isLewdSource -import exh.metadata.MetadataHelper -import exh.search.SearchEngine -import kotlinx.android.synthetic.main.item_catalogue_grid.view.* -import uy.kohesive.injekt.injectLazy -import java.util.* /** * Adapter storing a list of manga in a certain category. * - * @param fragment the fragment containing this adapter. + * @param view the fragment containing this adapter. */ -class LibraryCategoryAdapter(val fragment: LibraryCategoryView) : - FlexibleAdapter() { +class LibraryCategoryAdapter(view: LibraryCategoryView) : + FlexibleAdapter(null, view, true) { /** * The list of manga in this category. */ - private var mangas: List = emptyList() + private var mangas: List = emptyList() + //EH private val sourceManager: SourceManager by injectLazy() private val searchEngine = SearchEngine() @@ -41,33 +24,19 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) : var asyncSearchText: String? = null - init { - setHasStableIds(true) - } - /** * Sets a list of manga in the adapter. * * @param list the list to set. */ - fun setItems(list: List) { - mItems = list - + fun setItems(list: List) { // A copy of manga always unfiltered. - mangas = ArrayList(list) - updateDataSet(null) - } - - /** - * Returns the identifier for a manga. - * - * @param position the position in the adapter. - * @return an identifier for the item. - */ - override fun getItemId(position: Int): Long { - return mItems[position].id!! + mangas = list.toList() + + performFilter() } + // --> EH /** * Filters the list of manga applying [filterObject] for each element. * @@ -108,41 +77,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) : } } - /** - * Creates a new view holder. - * - * @param parent the parent view. - * @param viewType the type of the holder. - * @return a new view holder for a manga. - */ - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder { - // Depending on preferences, display a list or display a grid - if (parent is AutofitRecyclerView) { - val view = parent.inflate(R.layout.item_catalogue_grid).apply { - val coverHeight = parent.itemWidth / 3 * 4 - card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) - gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) - } - return LibraryGridHolder(view, this, fragment) - } else { - val view = parent.inflate(R.layout.item_catalogue_list) - return LibraryListHolder(view, this, fragment) - } - } - - /** - * Binds a holder with a new position. - * - * @param holder the holder to bind. - * @param position the position to bind. - */ - override fun onBindViewHolder(holder: LibraryHolder, position: Int) { - val manga = getItem(position) - - holder.onSetValues(manga) - // When user scrolls this bind the correct selection status - holder.itemView.isActivated = isSelected(position) - } + // <-- EH /** * Returns the position in the adapter for the given manga. @@ -150,7 +85,11 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) : * @param manga the manga to find. */ fun indexOf(manga: Manga): Int { - return mangas.orEmpty().indexOfFirst { it.id == manga.id } + return mangas.indexOfFirst { it.manga.id == manga.id } + } + + fun performFilter() { + updateDataSet(mangas.filter { it.filter(searchText) }) } } 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 dd7d2eaf2..ef3f3f247 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 @@ -5,30 +5,28 @@ import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView import android.util.AttributeSet import android.widget.FrameLayout -import eu.davidea.flexibleadapter4.FlexibleAdapter +import eu.davidea.flexibleadapter.FlexibleAdapter 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.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder -import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import kotlinx.android.synthetic.main.item_library_category.view.* -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.library_category.view.* +import rx.subscriptions.CompositeSubscription import uy.kohesive.injekt.injectLazy -import java.util.concurrent.TimeUnit /** * Fragment containing the library manga for a certain category. - * Uses R.layout.fragment_library_category. */ -class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) -: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener { +class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs), + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener { /** * Preferences. @@ -38,7 +36,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att /** * The fragment containing this view. */ - private lateinit var fragment: LibraryFragment + private lateinit var controller: LibraryController /** * Category for this view. @@ -57,22 +55,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att private lateinit var adapter: LibraryCategoryAdapter /** - * Subscription for the library manga. + * Subscriptions while the view is bound. */ - private var libraryMangaSubscription: Subscription? = null + private var subscriptions = CompositeSubscription() - /** - * Subscription of the library search. - */ - private var searchSubscription: Subscription? = null - - /** - * Subscription of the library selections. - */ - private var selectionSubscription: Subscription? = null - - fun onCreate(fragment: LibraryFragment) { - this.fragment = fragment + fun onCreate(controller: LibraryController) { + this.controller = controller recycler = if (preferences.libraryAsList().getOrDefault()) { (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { @@ -80,7 +68,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att } } else { (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { - spanCount = fragment.mangaPerRow + spanCount = controller.mangaPerRow } } @@ -95,7 +83,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att // Disable swipe refresh when view is not at the top val firstPos = (recycler.layoutManager as LinearLayoutManager) .findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos == 0 + swipe_refresh.isEnabled = firstPos <= 0 } }) @@ -114,38 +102,45 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att fun onBind(category: Category) { this.category = category + //TODO Fix + // --> EH val presenter = fragment.presenter searchSubscription = presenter .searchSubject .debounce(10L, TimeUnit.MILLISECONDS) .subscribe { text -> //Debounce search (EH) - adapter.asyncSearchText = text?.trim()?.toLowerCase() - adapter.updateDataSet() - } + adapter.asyncSearchText = text?.trim()?.toLowerCase() + adapter.updateDataSet() + } + // <-- EH - adapter.mode = if (presenter.selectedMangas.isNotEmpty()) { + adapter.mode = if (controller.selectedMangas.isNotEmpty()) { FlexibleAdapter.MODE_MULTI } else { FlexibleAdapter.MODE_SINGLE } - libraryMangaSubscription = presenter.libraryMangaSubject + subscriptions += controller.searchRelay + .doOnNext { adapter.searchText = it } + .skip(1) + .subscribe { adapter.performFilter() } + + subscriptions += controller.libraryMangaRelay .subscribe { onNextLibraryManga(it) } - selectionSubscription = presenter.selectionSubject + subscriptions += controller.selectionRelay .subscribe { onSelectionChanged(it) } } fun onRecycle() { adapter.setItems(emptyList()) adapter.clearSelection() + subscriptions.clear() } override fun onDetachedFromWindow() { - searchSubscription?.unsubscribe() - libraryMangaSubscription?.unsubscribe() - selectionSubscription?.unsubscribe() + subscriptions.clear() super.onDetachedFromWindow() } @@ -163,7 +158,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att adapter.setItems(mangaForCategory) if (adapter.mode == FlexibleAdapter.MODE_MULTI) { - fragment.presenter.selectedMangas.forEach { manga -> + controller.selectedMangas.forEach { manga -> val position = adapter.indexOf(manga) if (position != -1 && !adapter.isSelected(position)) { adapter.toggleSelection(position) @@ -189,7 +184,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att } is LibrarySelectionEvent.Unselected -> { findAndToggleSelection(event.manga) - if (fragment.presenter.selectedMangas.isEmpty()) { + if (controller.selectedMangas.isEmpty()) { adapter.mode = FlexibleAdapter.MODE_SINGLE } } @@ -219,14 +214,14 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att * @param position the position of the element clicked. * @return true if the item should be selected, false otherwise. */ - override fun onListItemClick(position: Int): Boolean { + 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) { toggleSelection(position) return true } else { - openManga(item) + openManga(item.manga) return false } } @@ -236,8 +231,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att * * @param position the position of the element clicked. */ - override fun onListItemLongClick(position: Int) { - fragment.createActionModeIfNeeded() + override fun onItemLongClick(position: Int) { + controller.createActionModeIfNeeded() toggleSelection(position) } @@ -247,25 +242,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att * @param manga the manga to open. */ private fun openManga(manga: Manga) { - // Notify the presenter a manga is being opened. - fragment.presenter.onOpenManga() - - // Create a new activity with the manga. - val intent = MangaActivity.newIntent(context, manga) - fragment.startActivity(intent) + controller.openManga(manga) } - /** * Tells the presenter to toggle the selection for the given position. * * @param position the position to toggle. */ private fun toggleSelection(position: Int) { - val manga = adapter.getItem(position) ?: return + val item = adapter.getItem(position) ?: return - fragment.presenter.setSelection(manga, !adapter.isSelected(position)) - fragment.invalidateActionMode() + 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 new file mode 100644 index 000000000..34b190d2d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -0,0 +1,534 @@ +package eu.kanade.tachiyomi.ui.library + +import android.app.Activity +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.os.Bundle +import android.support.design.widget.TabLayout +import android.support.v4.graphics.drawable.DrawableCompat +import android.support.v4.widget.DrawerLayout +import android.support.v7.app.AppCompatActivity +import android.support.v7.view.ActionMode +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.f2prateek.rx.preferences.Preference +import com.jakewharton.rxbinding.support.v4.view.pageSelections +import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import 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.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController +import eu.kanade.tachiyomi.ui.base.controller.TabbedController +import eu.kanade.tachiyomi.ui.category.CategoryController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener +import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.library_controller.view.* +import rx.Subscription +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException + + +class LibraryController( + bundle: Bundle? = null, + private val preferences: PreferencesHelper = Injekt.get() +) : NucleusController(bundle), + TabbedController, + SecondaryDrawerController, + ActionMode.Callback, + ChangeMangaCategoriesDialog.Listener, + DeleteLibraryMangasDialog.Listener { + + /** + * Position of the active category. + */ + var activeCategory: Int = preferences.lastUsedCategory().getOrDefault() + private set + + /** + * Action mode for selections. + */ + private var actionMode: ActionMode? = null + + /** + * Library search query. + */ + private var query = "" + + /** + * Currently selected mangas. + */ + val selectedMangas = mutableListOf() + + private var selectedCoverManga: Manga? = null + + /** + * Relay to notify the UI of selection updates. + */ + val selectionRelay: PublishRelay = PublishRelay.create() + + /** + * Relay to notify search query changes. + */ + val searchRelay: BehaviorRelay = BehaviorRelay.create() + + /** + * Relay to notify the library's viewpager for updates. + */ + val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() + + /** + * Number of manga per row in grid mode. + */ + var mangaPerRow = 0 + private set + + /** + * TabLayout of the categories. + */ + private val tabs: TabLayout? + get() = activity?.tabs + + private val drawer: DrawerLayout? + get() = activity?.drawer + + private var adapter: LibraryAdapter? = null + + /** + * Navigation view containing filter/sort/display items. + */ + private var navView: LibraryNavigationView? = null + + /** + * Drawer listener to allow swipe only for closing the drawer. + */ + private var drawerListener: DrawerLayout.DrawerListener? = null + + private var tabsVisibilityRelay: BehaviorRelay = BehaviorRelay.create(false) + + private var tabsVisibilitySubscription: Subscription? = null + + init { + setHasOptionsMenu(true) + } + + override fun getTitle(): String? { + return resources?.getString(R.string.label_library) + } + + override fun createPresenter(): LibraryPresenter { + return LibraryPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.library_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = LibraryAdapter(this) + with(view) { + view_pager.adapter = adapter + view_pager.pageSelections().skip(1).subscribeUntilDestroy { + preferences.lastUsedCategory().set(it) + activeCategory = it + } + + getColumnsPreferenceForCurrentOrientation().asObservable() + .doOnNext { mangaPerRow = it } + .skip(1) + // Set again the adapter to recalculate the covers height + .subscribeUntilDestroy { reattachAdapter() } + + if (selectedMangas.isNotEmpty()) { + createActionModeIfNeeded() + } + } + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type.isEnter) { + activity?.tabs?.setupWithViewPager(view?.view_pager) + } + } + + override fun onAttach(view: View) { + super.onAttach(view) + presenter.subscribeLibrary() + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + actionMode = null + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null + } + + override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { + val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView + drawerListener = DrawerSwipeCloseListener(drawer, view).also { + drawer.addDrawerListener(it) + } + navView = view + + navView?.post { + if (isAttached && drawer.isDrawerOpen(navView)) + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) + } + + navView?.onGroupClicked = { group -> + when (group) { + is LibraryNavigationView.FilterGroup -> onFilterChanged() + is LibraryNavigationView.SortGroup -> onSortChanged() + is LibraryNavigationView.DisplayGroup -> reattachAdapter() + } + } + + return view + } + + override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { + drawerListener?.let { drawer.removeDrawerListener(it) } + drawerListener = null + navView = null + } + + override fun configureTabs(tabs: TabLayout) { + with(tabs) { + tabGravity = TabLayout.GRAVITY_CENTER + tabMode = TabLayout.MODE_SCROLLABLE + } + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible -> + val tabAnimator = (activity as? MainActivity)?.tabAnimator + if (visible) { + tabAnimator?.expand() + } else { + tabAnimator?.collapse() + } + } + } + + override fun cleanupTabs(tabs: TabLayout) { + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null + } + + fun onNextLibraryUpdate(categories: List, mangaMap: Map>) { + val view = view ?: return + val adapter = adapter ?: return + + // Show empty view if needed + if (mangaMap.isNotEmpty()) { + view.empty_view.hide() + } else { + view.empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library) + } + + // Get the current active category. + val activeCat = if (adapter.categories.isNotEmpty()) + view.view_pager.currentItem + else + activeCategory + + // Set the categories + adapter.categories = categories + + // Restore active category. + view.view_pager.setCurrentItem(activeCat, false) + + tabsVisibilityRelay.call(categories.size > 1) + + // Delay the scroll position to allow the view to be properly measured. + view.post { + if (isAttached) { + tabs?.setScrollPosition(view.view_pager.currentItem, 0f, true) + } + } + + // Send the manga map to child fragments after the adapter is updated. + libraryMangaRelay.call(LibraryMangaEvent(mangaMap)) + } + + /** + * Returns a preference for the number of manga per row based on the current orientation. + * + * @return the preference. + */ + private fun getColumnsPreferenceForCurrentOrientation(): Preference { + return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) + preferences.portraitColumns() + else + preferences.landscapeColumns() + } + + /** + * Called when a filter is changed. + */ + private fun onFilterChanged() { + presenter.requestFilterUpdate() + (activity as? AppCompatActivity)?.supportInvalidateOptionsMenu() + } + + /** + * Called when the sorting mode is changed. + */ + private fun onSortChanged() { + presenter.requestSortUpdate() + } + + /** + * Reattaches the adapter to the view pager to recreate fragments + */ + private fun reattachAdapter() { + val pager = view?.view_pager ?: return + val adapter = adapter ?: return + + val position = pager.currentItem + + adapter.recycle = false + pager.adapter = adapter + pager.currentItem = position + adapter.recycle = true + } + + /** + * Creates the action mode if it's not created already. + */ + fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + } + } + + /** + * Destroys the action mode. + */ + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.library, menu) + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + + if (!query.isNullOrEmpty()) { + searchItem.expandActionView() + searchView.setQuery(query, true) + searchView.clearFocus() + } + + // Mutate the filter icon because it needs to be tinted and the resource is shared. + menu.findItem(R.id.action_filter).icon.mutate() + + searchView.queryTextChanges().subscribeUntilDestroy { + query = it.toString() + searchRelay.call(query) + } + + searchItem.fixExpand() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + val navView = navView ?: return + + val filterItem = menu.findItem(R.id.action_filter) + + // Tint icon if there's a filter active + val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE + DrawableCompat.setTint(filterItem.icon, filterColor) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_filter -> { + navView?.let { drawer?.openDrawer(Gravity.END) } + } + R.id.action_update_library -> { + activity?.let { LibraryUpdateService.start(it) } + } + R.id.action_edit_categories -> { + router.pushController(RouterTransaction.with(CategoryController()) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + else -> return super.onOptionsItemSelected(item) + } + + return true + } + + /** + * Invalidates the action mode, forcing it to refresh its content. + */ + fun invalidateActionMode() { + actionMode?.invalidate() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.library_selection, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = selectedMangas.size + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1 + } + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_edit_cover -> { + changeSelectedCover() + destroyActionModeIfNeeded() + } + R.id.action_move_to_category -> showChangeMangaCategoriesDialog() + R.id.action_delete -> showDeleteMangaDialog() + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) { + // Clear all the manga selections and notify child views. + selectedMangas.clear() + selectionRelay.call(LibrarySelectionEvent.Cleared()) + actionMode = null + } + + fun openManga(manga: Manga) { + // Notify the presenter a manga is being opened. + presenter.onOpenManga() + + router.pushController(RouterTransaction.with(MangaController(manga)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + + /** + * Sets the selection for a given manga. + * + * @param manga the manga whose selection has changed. + * @param selected whether it's now selected or not. + */ + fun setSelection(manga: Manga, selected: Boolean) { + if (selected) { + selectedMangas.add(manga) + selectionRelay.call(LibrarySelectionEvent.Selected(manga)) + } else { + selectedMangas.remove(manga) + selectionRelay.call(LibrarySelectionEvent.Unselected(manga)) + } + } + + /** + * Move the selected manga to a list of categories. + */ + private fun showChangeMangaCategoriesDialog() { + // Create a copy of selected manga + val mangas = selectedMangas.toList() + + // Hide the default category because it has a different behavior than the ones from db. + val categories = presenter.categories.filter { it.id != 0 } + + // Get indexes of the common categories to preselect. + val commonCategoriesIndexes = presenter.getCommonCategories(mangas) + .map { categories.indexOf(it) } + .toTypedArray() + + ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) + .showDialog(router, null) + } + + private fun showDeleteMangaDialog() { + DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router, null) + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + presenter.moveMangasToCategories(categories, mangas) + destroyActionModeIfNeeded() + } + + override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { + presenter.removeMangaFromLibrary(mangas, deleteChapters) + destroyActionModeIfNeeded() + } + + /** + * Changes the cover for the selected manga. + * + * @param mangas a list of selected manga. + */ + private fun changeSelectedCover() { + val manga = selectedMangas.firstOrNull() ?: return + selectedCoverManga = manga + + if (manga.favorite) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + startActivityForResult(Intent.createChooser(intent, + resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN) + } else { + activity?.toast(R.string.notification_first_add_to_library) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_IMAGE_OPEN) { + if (data == null || resultCode != Activity.RESULT_OK) return + val activity = activity ?: return + val manga = selectedCoverManga ?: return + + try { + // Get the file's input stream from the incoming Intent + activity.contentResolver.openInputStream(data.data).use { + // Update cover to selected file, show error if something went wrong + if (presenter.editCoverWithStream(it, manga)) { + // TODO refresh cover + } else { + activity.toast(R.string.notification_cover_update_failed) + } + } + } catch (error: IOException) { + activity.toast(R.string.notification_cover_update_failed) + Timber.e(error) + } + selectedCoverManga = null + } + } + + private companion object { + /** + * Key to change the cover of a manga in [onActivityResult]. + */ + const val REQUEST_IMAGE_OPEN = 101 + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt deleted file mode 100755 index 2d334506f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ /dev/null @@ -1,509 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.app.Activity -import android.content.Intent -import android.content.res.Configuration -import android.graphics.Color -import android.os.Bundle -import android.support.design.widget.TabLayout -import android.support.v4.graphics.drawable.DrawableCompat -import android.support.v4.view.ViewPager -import android.support.v4.widget.DrawerLayout -import android.support.v7.app.AppCompatActivity -import android.support.v7.view.ActionMode -import android.support.v7.widget.SearchView -import android.view.* -import com.afollestad.materialdialogs.MaterialDialog -import com.f2prateek.rx.preferences.Preference -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.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.category.CategoryActivity -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.inflate -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.DialogCheckboxView -import exh.FavoritesSyncHelper -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.fragment_library.* -import nucleus.factory.RequiresPresenter -import rx.Subscription -import timber.log.Timber -import uy.kohesive.injekt.injectLazy -import java.io.IOException - -/** - * Fragment that shows the manga from the library. - * Uses R.layout.fragment_library. - */ -@RequiresPresenter(LibraryPresenter::class) -class LibraryFragment : BaseRxFragment(), ActionMode.Callback { - - /** - * Adapter containing the categories of the library. - */ - lateinit var adapter: LibraryAdapter - private set - - /** - * Preferences. - */ - val preferences: PreferencesHelper by injectLazy() - - /** - * TabLayout of the categories. - */ - private val tabs: TabLayout - get() = (activity as MainActivity).tabs - - /** - * Position of the active category. - */ - private var activeCategory: Int = 0 - - /** - * Query of the search box. - */ - private var query: String? = null - - /** - * Action mode for manga selection. - */ - private var actionMode: ActionMode? = null - - /** - * Selected manga for editing its cover. - */ - private var selectedCoverManga: Manga? = null - - /** - * Number of manga per row in grid mode. - */ - var mangaPerRow = 0 - private set - - /** - * Navigation view containing filter/sort/display items. - */ - private lateinit var navView: LibraryNavigationView - - /** - * Drawer listener to allow swipe only for closing the drawer. - */ - private val drawerListener by lazy { - object : DrawerLayout.SimpleDrawerListener() { - override fun onDrawerClosed(drawerView: View) { - if (drawerView == navView) { - activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) - } - } - - override fun onDrawerOpened(drawerView: View) { - if (drawerView == navView) { - activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView) - } - } - } - } - - /** - * Subscription for the number of manga per row. - */ - private var numColumnsSubscription: Subscription? = null - - companion object { - /** - * Key to change the cover of a manga in [onActivityResult]. - */ - const val REQUEST_IMAGE_OPEN = 101 - - /** - * Key to save and restore [query] from a [Bundle]. - */ - const val QUERY_KEY = "query_key" - - /** - * Key to save and restore [activeCategory] from a [Bundle]. - */ - const val CATEGORY_KEY = "category_key" - - /** - * Creates a new instance of this fragment. - * - * @return a new instance of [LibraryFragment]. - */ - fun newInstance(): LibraryFragment { - return LibraryFragment() - } - } - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_library, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - setToolbarTitle(getString(R.string.label_library)) - - adapter = LibraryAdapter(this) - view_pager.adapter = adapter - view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { - override fun onPageSelected(position: Int) { - preferences.lastUsedCategory().set(position) - } - }) - tabs.setupWithViewPager(view_pager) - - if (savedState != null) { - activeCategory = savedState.getInt(CATEGORY_KEY) - query = savedState.getString(QUERY_KEY) - presenter.searchSubject.call(query) - if (presenter.selectedMangas.isNotEmpty()) { - createActionModeIfNeeded() - } - } else { - activeCategory = preferences.lastUsedCategory().getOrDefault() - } - - numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() - .doOnNext { mangaPerRow = it } - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribe { reattachAdapter() } - - - // Inflate and prepare drawer - navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView - activity.drawer.addView(navView) - activity.drawer.addDrawerListener(drawerListener) - - navView.post { - if (isAdded && !activity.drawer.isDrawerOpen(navView)) - activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) - } - - navView.onGroupClicked = { group -> - when (group) { - is LibraryNavigationView.FilterGroup -> onFilterChanged() - is LibraryNavigationView.SortGroup -> onSortChanged() - is LibraryNavigationView.DisplayGroup -> reattachAdapter() - } - } - } - - override fun onResume() { - super.onResume() - presenter.subscribeLibrary() - } - - override fun onDestroyView() { - activity.drawer.removeDrawerListener(drawerListener) - activity.drawer.removeView(navView) - numColumnsSubscription?.unsubscribe() - tabs.setupWithViewPager(null) - tabs.visibility = View.GONE - super.onDestroyView() - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(CATEGORY_KEY, view_pager.currentItem) - outState.putString(QUERY_KEY, query) - super.onSaveInstanceState(outState) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.library, menu) - - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - - if (!query.isNullOrEmpty()) { - searchItem.expandActionView() - searchView.setQuery(query, true) - searchView.clearFocus() - } - - // Mutate the filter icon because it needs to be tinted and the resource is shared. - menu.findItem(R.id.action_filter).icon.mutate() - - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - onSearchTextChange(query) - return true - } - - override fun onQueryTextChange(newText: String): Boolean { - onSearchTextChange(newText) - return true - } - }) - - } - - override fun onPrepareOptionsMenu(menu: Menu) { - val filterItem = menu.findItem(R.id.action_filter) - - // Tint icon if there's a filter active - val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE - DrawableCompat.setTint(filterItem.icon, filterColor) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_filter -> { - activity.drawer.openDrawer(Gravity.END) - } - R.id.action_update_library -> { - LibraryUpdateService.start(activity) - } - R.id.action_edit_categories -> { - val intent = CategoryActivity.newIntent(activity) - startActivity(intent) - } - R.id.action_sync -> { - FavoritesSyncHelper(this.activity).guiSyncFavorites({ - //Do we even need stuff in here? - }) - } - else -> return super.onOptionsItemSelected(item) - } - - return true - } - - /** - * Called when a filter is changed. - */ - private fun onFilterChanged() { - presenter.requestFilterUpdate() - activity.supportInvalidateOptionsMenu() - } - - /** - * Called when the sorting mode is changed. - */ - private fun onSortChanged() { - presenter.requestSortUpdate() - } - - /** - * Reattaches the adapter to the view pager to recreate fragments - */ - private fun reattachAdapter() { - val position = view_pager.currentItem - adapter.recycle = false - view_pager.adapter = adapter - view_pager.currentItem = position - adapter.recycle = true - } - - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - private fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) - preferences.portraitColumns() - else - preferences.landscapeColumns() - } - - /** - * Updates the query. - * - * @param query the new value of the query. - */ - private fun onSearchTextChange(query: String?) { - this.query = query - - // Notify the subject the query has changed. - if (isResumed) { - presenter.searchSubject.call(query) - } - } - - /** - * Called when the library is updated. It sets the new data and updates the view. - * - * @param categories the categories of the library. - * @param mangaMap a map containing the manga for each category. - */ - fun onNextLibraryUpdate(categories: List, mangaMap: Map>) { - // Check if library is empty and update information accordingly. - (activity as MainActivity).updateEmptyView(mangaMap.isEmpty(), - R.string.information_empty_library, R.drawable.ic_book_black_128dp) - - // Get the current active category. - val activeCat = if (adapter.categories.isNotEmpty()) view_pager.currentItem else activeCategory - - // Set the categories - adapter.categories = categories - tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE - - // Restore active category. - view_pager.setCurrentItem(activeCat, false) - // Delay the scroll position to allow the view to be properly measured. - view_pager.post { if (isAdded) tabs.setScrollPosition(view_pager.currentItem, 0f, true) } - - // Send the manga map to child fragments after the adapter is updated. - presenter.libraryMangaSubject.call(LibraryMangaEvent(mangaMap)) - } - - /** - * Creates the action mode if it's not created already. - */ - fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - } - } - - /** - * Destroys the action mode. - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - /** - * Invalidates the action mode, forcing it to refresh its content. - */ - fun invalidateActionMode() { - actionMode?.invalidate() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.library_selection, menu) - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = presenter.selectedMangas.size - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = getString(R.string.label_selected, count) - menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1 - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_edit_cover -> { - changeSelectedCover(presenter.selectedMangas) - destroyActionModeIfNeeded() - } - R.id.action_move_to_category -> moveMangasToCategories(presenter.selectedMangas) - R.id.action_delete -> showDeleteMangaDialog() - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - presenter.clearSelections() - actionMode = null - } - - /** - * Changes the cover for the selected manga. - * - * @param mangas a list of selected manga. - */ - private fun changeSelectedCover(mangas: List) { - if (mangas.size == 1) { - selectedCoverManga = mangas[0] - if (selectedCoverManga?.favorite ?: false) { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "image/*" - startActivityForResult(Intent.createChooser(intent, - getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN) - } else { - context.toast(R.string.notification_first_add_to_library) - } - - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMAGE_OPEN) { - selectedCoverManga?.let { manga -> - - try { - // Get the file's input stream from the incoming Intent - context.contentResolver.openInputStream(data.data).use { - // Update cover to selected file, show error if something went wrong - if (presenter.editCoverWithStream(it, manga)) { - // TODO refresh cover - } else { - context.toast(R.string.notification_cover_update_failed) - } - } - } catch (error: IOException) { - context.toast(R.string.notification_cover_update_failed) - Timber.e(error) - } - } - - } - } - - /** - * Move the selected manga to a list of categories. - * - * @param mangas the manga list to move. - */ - private fun moveMangasToCategories(mangas: List) { - // Hide the default category because it has a different behavior than the ones from db. - val categories = presenter.categories.filter { it.id != 0 } - - // Get indexes of the common categories to preselect. - val commonCategoriesIndexes = presenter.getCommonCategories(mangas) - .map { categories.indexOf(it) } - .toTypedArray() - - MaterialDialog.Builder(activity) - .title(R.string.action_move_category) - .items(categories.map { it.name }) - .itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text -> - val selectedCategories = positions.map { categories[it] } - presenter.moveMangasToCategories(selectedCategories, mangas) - destroyActionModeIfNeeded() - true - } - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .show() - } - - private fun showDeleteMangaDialog() { - val view = DialogCheckboxView(context).apply { - setDescription(R.string.confirm_delete_manga) - setOptionDescription(R.string.also_delete_chapters) - } - - MaterialDialog.Builder(activity) - .title(R.string.action_remove) - .customView(view, true) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { dialog, action -> - val deleteChapters = view.isChecked() - presenter.removeMangaFromLibrary(deleteChapters) - destroyActionModeIfNeeded() - } - .show() - } - -} 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 91424f3bd..fcf3789e4 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,49 +1,49 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder -import kotlinx.android.synthetic.main.item_catalogue_grid.view.* - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_catalogue_grid" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. - */ -class LibraryGridHolder(private val view: View, - private val adapter: LibraryCategoryAdapter, - listener: FlexibleViewHolder.OnListItemClickListener) -: LibraryHolder(view, adapter, listener) { - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - override fun onSetValues(manga: Manga) { - // Update the title of the manga. - view.title.text = 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() - } - - // Update the cover. - Glide.clear(view.thumbnail) - Glide.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(view.thumbnail) - } - -} +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 kotlinx.android.synthetic.main.catalogue_grid_item.view.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_catalogue_grid" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ +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. + */ + override fun onSetValues(manga: Manga) { + // Update the title of the manga. + view.title.text = 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() + } + + // Update the cover. + Glide.clear(view.thumbnail) + Glide.with(view.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .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 efdd42200..2359377da 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 @@ -1,27 +1,28 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder - -/** - * Generic class used to hold the displayed data of a manga in the library. - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to the single tap and long tap events. - */ - -abstract class LibraryHolder(private val view: View, - adapter: LibraryCategoryAdapter, - listener: FlexibleViewHolder.OnListItemClickListener) -: FlexibleViewHolder(view, adapter, listener) { - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - abstract fun onSetValues(manga: Manga) - -} +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. + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to the single tap and long tap events. + */ + +abstract class LibraryHolder( + view: View, + adapter: FlexibleAdapter<*> +) : FlexibleViewHolder(view, adapter) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param manga the manga to bind. + */ + abstract fun onSetValues(manga: Manga) + +} 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 new file mode 100644 index 000000000..2e78c001d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -0,0 +1,70 @@ +package eu.kanade.tachiyomi.ui.library + +import android.view.Gravity +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +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.widget.AutofitRecyclerView +import kotlinx.android.synthetic.main.catalogue_grid_item.view.* + +class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFilterable { + + override fun getLayoutRes(): Int { + return R.layout.catalogue_grid_item + } + + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): LibraryHolder { + + return if (parent is AutofitRecyclerView) { + val view = parent.inflate(R.layout.catalogue_grid_item).apply { + val coverHeight = parent.itemWidth / 3 * 4 + card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) + gradient.layoutParams = FrameLayout.LayoutParams( + MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) + } + LibraryGridHolder(view, adapter) + } else { + val view = parent.inflate(R.layout.catalogue_list_item) + LibraryListHolder(view, adapter) + } + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: LibraryHolder, + position: Int, + payloads: List?) { + + holder.onSetValues(manga) + } + + /** + * Filters a manga depending on a query. + * + * @param constraint the query to apply. + * @return true if the manga should be included, false otherwise. + */ + override fun filter(constraint: String): Boolean { + return manga.title.contains(constraint, true) || + (manga.author?.contains(constraint, true) ?: false) + } + + override fun equals(other: Any?): Boolean { + if (other is LibraryItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id!!.hashCode() + } +} \ No newline at end of file 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 22dd444c9..6de899532 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,57 +1,59 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.View -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder -import kotlinx.android.synthetic.main.item_catalogue_list.view.* - -/** - * Class used to hold the displayed data of a manga in the library, like the cover or the title. - * All the elements from the layout file "item_library_list" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @param listener a listener to react to single tap and long tap events. - * @constructor creates a new library holder. - */ - -class LibraryListHolder(private val view: View, - private val adapter: LibraryCategoryAdapter, - listener: FlexibleViewHolder.OnListItemClickListener) -: LibraryHolder(view, adapter, listener) { - - /** - * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - override fun onSetValues(manga: Manga) { - // Update the title of the manga. - itemView.title.text = 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() - } - - // Create thumbnail onclick to simulate long click - itemView.thumbnail.setOnClickListener { - // Simulate long click on this view to enter selection mode - onLongClick(itemView) - } - - // Update the cover. - Glide.clear(itemView.thumbnail) - Glide.with(itemView.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .dontAnimate() - .into(itemView.thumbnail) - } - +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 kotlinx.android.synthetic.main.catalogue_list_item.view.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_library_list" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ + +class LibraryListHolder( + 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. + */ + override fun onSetValues(manga: Manga) { + // Update the title of the manga. + itemView.title.text = 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() + } + + // Create thumbnail onclick to simulate long click + itemView.thumbnail.setOnClickListener { + // Simulate long click on this view to enter selection mode + onLongClick(itemView) + } + + // Update the cover. + Glide.clear(itemView.thumbnail) + Glide.with(itemView.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .bitmapTransform(CropCircleTransformation(itemView.context)) + .dontAnimate() + .into(itemView.thumbnail) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt index 2b1be71e1..e5dffe308 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt @@ -1,11 +1,10 @@ package eu.kanade.tachiyomi.ui.library import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -class LibraryMangaEvent(val mangas: Map>) { +class LibraryMangaEvent(val mangas: Map>) { - fun getMangaForCategory(category: Category): List? { + fun getMangaForCategory(category: Category): List? { return mangas[category.id] } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt index 8c3cb176c..6c1d4a040 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 @@ -74,7 +74,9 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) - override val items = listOf(downloaded, unread) + private val completed = Item.CheckboxGroup(R.string.completed, this) + + override val items = listOf(downloaded, unread, completed) override val header = Item.Header(R.string.action_filter) @@ -83,6 +85,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A override fun initModels() { downloaded.checked = preferences.filterDownloaded().getOrDefault() unread.checked = preferences.filterUnread().getOrDefault() + completed.checked = preferences.filterCompleted().getOrDefault() } override fun onItemClicked(item: Item) { @@ -91,6 +94,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A when (item) { downloaded -> preferences.filterDownloaded().set(item.checked) unread -> preferences.filterUnread().set(item.checked) + completed -> preferences.filterCompleted().set(item.checked) } adapter.notifyItemChanged(item) @@ -105,13 +109,15 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) + private val total = Item.MultiSort(R.string.action_sort_total, this) + private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this) private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this) private val unread = Item.MultiSort(R.string.action_filter_unread, this) - override val items = listOf(alphabetically, lastRead, lastUpdated, unread) + override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total) override val header = Item.Header(R.string.action_sort) @@ -126,6 +132,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE + total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE } override fun onItemClicked(item: Item) { @@ -145,6 +152,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A lastRead -> LibrarySort.LAST_READ lastUpdated -> LibrarySort.LAST_UPDATED unread -> LibrarySort.UNREAD + total -> LibrarySort.TOTAL else -> throw Exception("Unknown sorting") }) preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false) 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 8be382b6f..7661f218f 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,373 +1,331 @@ -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 com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.combineLatest -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy -import java.io.IOException -import java.io.InputStream -import java.util.* - -/** - * Presenter of [LibraryFragment]. - */ -class LibraryPresenter : BasePresenter() { - - /** - * Database. - */ - private val db: DatabaseHelper by injectLazy() - - /** - * Preferences. - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * Cover cache. - */ - private val coverCache: CoverCache by injectLazy() - - /** - * Source manager. - */ - private val sourceManager: SourceManager by injectLazy() - - /** - * Download manager. - */ - private val downloadManager: DownloadManager by injectLazy() - - /** - * Categories of the library. - */ - var categories: List = emptyList() - - /** - * Currently selected manga. - */ - val selectedMangas = mutableListOf() - - /** - * Search query of the library. - */ - val searchSubject: BehaviorRelay = BehaviorRelay.create() - - /** - * Subject to notify the library's viewpager for updates. - */ - val libraryMangaSubject: BehaviorRelay = BehaviorRelay.create() - - /** - * Subject to notify the UI of selection updates. - */ - val selectionSubject: PublishRelay = PublishRelay.create() - - /** - * Relay used to apply the UI filters to the last emission of the library. - */ - private val filterTriggerRelay = BehaviorRelay.create(Unit) - - /** - * Relay used to apply the selected sorting method to the last emission of the library. - */ - private val sortTriggerRelay = BehaviorRelay.create(Unit) - - /** - * Library subscription. - */ - private var librarySubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - subscribeLibrary() - } - - /** - * Subscribes to library if needed. - */ - fun subscribeLibrary() { - if (librarySubscription.isNullOrUnsubscribed()) { - librarySubscription = getLibraryObservable() - .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), - { lib, tick -> Pair(lib.first, applyFilters(lib.second)) }) - .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), - { lib, tick -> Pair(lib.first, applySort(lib.second)) }) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, pair -> - view.onNextLibraryUpdate(pair.first, pair.second) - }) - } - } - - /** - * Applies library filters to the given map of manga. - * - * @param map the map to filter. - */ - private fun applyFilters(map: Map>): Map> { - // 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 filterDownloaded = preferences.filterDownloaded().getOrDefault() - - val filterUnread = preferences.filterUnread().getOrDefault() - - val filterFn: (Manga) -> Boolean = f@ { manga -> - // Filter out manga without source. - val source = sourceManager.get(manga.source) ?: return@f false - - // Filter when there isn't unread chapters. - if (filterUnread && manga.unread == 0) { - return@f false - } - - // Filter when the download directory doesn't exist or is null. - if (filterDownloaded) { - // 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 mangaDir = dirsForSource[mangaDirName] ?: return@f false - - val hasDirs = chapterDirectories.getOrPut(manga.id!!) { - mangaDir.listFiles()?.isNotEmpty() ?: false - } - if (!hasDirs) { - return@f false - } - } - true - } - - return map.mapValues { entry -> entry.value.filter(filterFn) } - } - - /** - * Applies library sorting to the given map of manga. - * - * @param map the map to sort. - */ - private fun applySort(map: Map>): Map> { - val sortingMode = preferences.librarySortingMode().getOrDefault() - - val lastReadManga by lazy { - var counter = 0 - db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ } - } - - val sortFn: (Manga, Manga) -> Int = { manga1, manga2 -> - when (sortingMode) { - LibrarySort.ALPHA -> manga1.title.compareTo(manga2.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 - manga1LastRead.compareTo(manga2LastRead) - } - LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update) - LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread) - else -> throw Exception("Unknown sorting mode") - } - } - - val comparator = if (preferences.librarySortingAscending().getOrDefault()) - Comparator(sortFn) - else - Collections.reverseOrder(sortFn) - - return map.mapValues { entry -> entry.value.sortedWith(comparator) } - } - - /** - * Get the categories and all its manga from the database. - * - * @return an observable of the categories and its manga. - */ - private fun getLibraryObservable(): Observable, Map>>> { - return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), - { dbCategories, libraryManga -> - val categories = if (libraryManga.containsKey(0)) - arrayListOf(Category.createDefault()) + dbCategories - else - dbCategories - - this.categories = categories - Pair(categories, libraryManga) - }) - } - - /** - * Get the categories from the database. - * - * @return an observable of the categories. - */ - private fun getCategoriesObservable(): Observable> { - return db.getCategories().asRxObservable() - } - - /** - * Get the manga grouped by categories. - * - * @return an observable containing a map with the category id as key and a list of manga as the - * value. - */ - private fun getLibraryMangasObservable(): Observable>> { - return db.getLibraryMangas().asRxObservable() - .map { list -> list.groupBy { it.category } } - } - - /** - * Requests the library to be filtered. - */ - fun requestFilterUpdate() { - filterTriggerRelay.call(Unit) - } - - /** - * Requests the library to be sorted. - */ - fun requestSortUpdate() { - sortTriggerRelay.call(Unit) - } - - /** - * Called when a manga is opened. - */ - fun onOpenManga() { - // Avoid further db updates for the library when it's not needed - librarySubscription?.let { remove(it) } - } - - /** - * Sets the selection for a given manga. - * - * @param manga the manga whose selection has changed. - * @param selected whether it's now selected or not. - */ - fun setSelection(manga: Manga, selected: Boolean) { - if (selected) { - selectedMangas.add(manga) - selectionSubject.call(LibrarySelectionEvent.Selected(manga)) - } else { - selectedMangas.remove(manga) - selectionSubject.call(LibrarySelectionEvent.Unselected(manga)) - } - } - - /** - * Clears all the manga selections and notifies the UI. - */ - fun clearSelections() { - selectedMangas.clear() - selectionSubject.call(LibrarySelectionEvent.Cleared()) - } - - /** - * Returns the common categories for the given list of manga. - * - * @param mangas the list of manga. - */ - fun getCommonCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas.toSet() - .map { db.getCategoriesForManga(it).executeAsBlocking() } - .reduce { set1: Iterable, set2 -> set1.intersect(set2) } - } - - /** - * Remove the selected manga from the library. - * - * @param deleteChapters whether to also delete downloaded chapters. - */ - fun removeMangaFromLibrary(deleteChapters: Boolean) { - // Create a set of the list - val mangaToDelete = selectedMangas.distinctBy { it.id } - mangaToDelete.forEach { it.favorite = false } - - Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } - .onErrorResumeNext { Observable.empty() } - .subscribeOn(Schedulers.io()) - .subscribe() - - Observable.fromCallable { - mangaToDelete.forEach { manga -> - coverCache.deleteFromCache(manga.thumbnail_url) - if (deleteChapters) { - val source = sourceManager.get(manga.source) as? HttpSource - if (source != null) { - downloadManager.findMangaDir(source, manga)?.delete() - } - } - } - } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Move the given list of manga to categories. - * - * @param categories the selected categories. - * @param mangas the list of manga to move. - */ - fun moveMangasToCategories(categories: List, mangas: List) { - val mc = ArrayList() - - for (manga in mangas) { - for (cat in categories) { - mc.add(MangaCategory.create(manga, cat)) - } - } - - db.setMangaCategories(mc, mangas) - } - - /** - * Update cover with local file. - * - * @param inputStream the new cover. - * @param manga the manga edited. - * @return true if the cover is updated, false otherwise - */ - @Throws(IOException::class) - fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { - if (manga.source == LocalSource.ID) { - LocalSource.updateCover(context, manga, inputStream) - return true - } - - if (manga.thumbnail_url != null && manga.favorite) { - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - return true - } - return false - } - -} +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 +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.combineLatest +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException +import java.io.InputStream +import java.util.* + +/** + * Presenter of [LibraryController]. + */ +class LibraryPresenter( + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + /** + * Categories of the library. + */ + var categories: List = emptyList() + private set + + /** + * Relay used to apply the UI filters to the last emission of the library. + */ + private val filterTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Relay used to apply the selected sorting method to the last emission of the library. + */ + private val sortTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Library subscription. + */ + private var librarySubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + subscribeLibrary() + } + + /** + * Subscribes to library if needed. + */ + fun subscribeLibrary() { + if (librarySubscription.isNullOrUnsubscribed()) { + librarySubscription = getLibraryObservable() + .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> Pair(lib.first, applyFilters(lib.second)) }) + .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> Pair(lib.first, applySort(lib.second)) }) + .map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache({ view, pair -> + view.onNextLibraryUpdate(pair.first, pair.second) + }) + } + } + + /** + * Applies library filters to the given map of manga. + * + * @param map the map to filter. + */ + private fun applyFilters(map: Map>): Map> { + // 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 filterDownloaded = preferences.filterDownloaded().getOrDefault() + + val filterUnread = preferences.filterUnread().getOrDefault() + + val filterCompleted = preferences.filterCompleted().getOrDefault() + + val filterFn: (Manga) -> Boolean = f@ { manga -> + // Filter out manga without source. + val source = sourceManager.get(manga.source) ?: return@f false + + // Filter when there isn't unread chapters. + if (filterUnread && manga.unread == 0) { + return@f false + } + + if (filterCompleted && manga.status != SManga.COMPLETED) { + return@f false + } + + // Filter when the download directory doesn't exist or is null. + if (filterDownloaded) { + // 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 mangaDir = dirsForSource[mangaDirName] ?: return@f false + + val hasDirs = chapterDirectories.getOrPut(manga.id!!) { + mangaDir.listFiles()?.isNotEmpty() ?: false + } + if (!hasDirs) { + return@f false + } + } + true + } + + return map.mapValues { entry -> entry.value.filter(filterFn) } + } + + /** + * Applies library sorting to the given map of manga. + * + * @param map the map to sort. + */ + private fun applySort(map: Map>): Map> { + val sortingMode = preferences.librarySortingMode().getOrDefault() + + val lastReadManga by lazy { + var counter = 0 + db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ } + } + val totalChapterManga by lazy { + var counter = 0 + db.getTotalChapterManga().executeAsBlocking().associate { it.id!! to counter++ } + } + + val sortFn: (Manga, Manga) -> Int = { manga1, manga2 -> + when (sortingMode) { + LibrarySort.ALPHA -> manga1.title.compareTo(manga2.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 + manga1LastRead.compareTo(manga2LastRead) + } + LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update) + LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread) + LibrarySort.TOTAL -> { + val manga1TotalChapter = totalChapterManga[manga1.id!!] ?: 0 + val mange2TotalChapter = totalChapterManga[manga2.id!!] ?: 0 + manga1TotalChapter.compareTo(mange2TotalChapter) + } + else -> throw Exception("Unknown sorting mode") + } + } + + val comparator = if (preferences.librarySortingAscending().getOrDefault()) + Comparator(sortFn) + else + Collections.reverseOrder(sortFn) + + return map.mapValues { entry -> entry.value.sortedWith(comparator) } + } + + /** + * Get the categories and all its manga from the database. + * + * @return an observable of the categories and its manga. + */ + private fun getLibraryObservable(): Observable, Map>>> { + return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), + { dbCategories, libraryManga -> + val categories = if (libraryManga.containsKey(0)) + arrayListOf(Category.createDefault()) + dbCategories + else + dbCategories + + this.categories = categories + Pair(categories, libraryManga) + }) + } + + /** + * Get the categories from the database. + * + * @return an observable of the categories. + */ + private fun getCategoriesObservable(): Observable> { + return db.getCategories().asRxObservable() + } + + /** + * Get the manga grouped by categories. + * + * @return an observable containing a map with the category id as key and a list of manga as the + * value. + */ + private fun getLibraryMangasObservable(): Observable>> { + return db.getLibraryMangas().asRxObservable() + .map { list -> list.groupBy { it.category } } + } + + /** + * Requests the library to be filtered. + */ + fun requestFilterUpdate() { + filterTriggerRelay.call(Unit) + } + + /** + * Requests the library to be sorted. + */ + fun requestSortUpdate() { + sortTriggerRelay.call(Unit) + } + + /** + * Called when a manga is opened. + */ + fun onOpenManga() { + // Avoid further db updates for the library when it's not needed + librarySubscription?.let { remove(it) } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas.toSet() + .map { db.getCategoriesForManga(it).executeAsBlocking() } + .reduce { set1: Iterable, set2 -> set1.intersect(set2) } + } + + /** + * Remove the selected manga from the library. + * + * @param mangas the list of manga to delete. + * @param deleteChapters whether to also delete downloaded chapters. + */ + fun removeMangaFromLibrary(mangas: List, deleteChapters: Boolean) { + // Create a set of the list + val mangaToDelete = mangas.distinctBy { it.id } + mangaToDelete.forEach { it.favorite = false } + + Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } + .onErrorResumeNext { Observable.empty() } + .subscribeOn(Schedulers.io()) + .subscribe() + + Observable.fromCallable { + mangaToDelete.forEach { manga -> + coverCache.deleteFromCache(manga.thumbnail_url) + if (deleteChapters) { + val source = sourceManager.get(manga.source) as? HttpSource + if (source != null) { + downloadManager.findMangaDir(source, manga)?.delete() + } + } + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Move the given list of manga to categories. + * + * @param categories the selected categories. + * @param mangas the list of manga to move. + */ + fun moveMangasToCategories(categories: List, mangas: List) { + val mc = ArrayList() + + for (manga in mangas) { + for (cat in categories) { + mc.add(MangaCategory.create(manga, cat)) + } + } + + db.setMangaCategories(mc, mangas) + } + + /** + * Update cover with local file. + * + * @param inputStream the new cover. + * @param manga the manga edited. + * @return true if the cover is updated, false otherwise + */ + @Throws(IOException::class) + fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { + if (manga.source == LocalSource.ID) { + LocalSource.updateCover(context, manga, inputStream) + return true + } + + if (manga.thumbnail_url != null && manga.favorite) { + coverCache.copyToCache(manga.thumbnail_url!!, inputStream) + return true + } + return false + } + +} 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 35cff3a98..677eeb244 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 @@ -6,5 +6,5 @@ object LibrarySort { const val LAST_READ = 1 const val LAST_UPDATED = 2 const val UNREAD = 3 - + const val TOTAL = 4 } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt new file mode 100644 index 000000000..4d24b7e20 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogController.kt @@ -0,0 +1,32 @@ +package eu.kanade.tachiyomi.ui.main + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView + +class ChangelogDialogController : DialogController() { + + override fun onCreateDialog(savedState: Bundle?): Dialog { + val activity = activity!! + val view = WhatsNewRecyclerView(activity) + return MaterialDialog.Builder(activity) + .title(if (BuildConfig.DEBUG) "Notices" else "Changelog") + .customView(view, false) + .positiveText(android.R.string.yes) + .build() + } + + class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) { + override fun initAttrs(attrs: AttributeSet?, defStyle: Int) { + mRowLayoutId = R.layout.changelog_row_layout + mRowHeaderLayoutId = R.layout.changelog_header_layout + mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogFragment.kt deleted file mode 100755 index 1c6c35861..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogFragment.kt +++ /dev/null @@ -1,74 +0,0 @@ -package eu.kanade.tachiyomi.ui.main - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.support.v4.app.DialogFragment -import android.support.v4.app.FragmentManager -import android.util.AttributeSet -import com.afollestad.materialdialogs.MaterialDialog -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob -import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView -import java.io.File - -class ChangelogDialogFragment : DialogFragment() { - - companion object { - fun show(context: Context, preferences: PreferencesHelper, fm: FragmentManager) { - val oldVersion = preferences.lastVersionCode().getOrDefault() - if (oldVersion < BuildConfig.VERSION_CODE) { - preferences.lastVersionCode().set(BuildConfig.VERSION_CODE) - ChangelogDialogFragment().show(fm, "changelog") - - if (oldVersion == 0) return - - if (oldVersion < 14) { - // Restore jobs after upgrading to evernote's job scheduler. - if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) { - UpdateCheckerJob.setupTask() - } - LibraryUpdateJob.setupTask() - } - if (oldVersion < 15) { - // Delete internal chapter cache dir. - File(context.cacheDir, "chapter_disk_cache").deleteRecursively() - } - if (oldVersion < 19) { - // Move covers to external files dir. - val oldDir = File(context.externalCacheDir, "cover_disk_cache") - if (oldDir.exists()) { - val destDir = context.getExternalFilesDir("covers") - if (destDir != null) { - oldDir.listFiles().forEach { - it.renameTo(File(destDir, it.name)) - } - } - } - } - //TODO Review any other changes below - } - } - } - - override fun onCreateDialog(savedState: Bundle?): Dialog { - val view = WhatsNewRecyclerView(context) - return MaterialDialog.Builder(activity) - .title(if (BuildConfig.DEBUG) "Notices" else "Changelog") - .customView(view, false) - .positiveText(android.R.string.yes) - .build() - } - - class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) { - override fun initAttrs(attrs: AttributeSet?, defStyle: Int) { - mRowLayoutId = R.layout.changelog_row_layout - mRowHeaderLayoutId = R.layout.changelog_header_layout - mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release - } - } -} \ 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 ef457c843..50e7b5542 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 @@ -1,45 +1,62 @@ package eu.kanade.tachiyomi.ui.main +import android.animation.ObjectAnimator import android.content.Intent +import android.graphics.Color import android.os.Bundle -import android.support.v4.app.Fragment -import android.support.v4.app.TaskStackBuilder import android.support.v4.view.GravityCompat -import android.view.MenuItem +import android.support.v4.widget.DrawerLayout +import android.support.v7.graphics.drawable.DrawerArrowDrawable +import android.view.ViewGroup +import com.bluelinelabs.conductor.* +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment -import eu.kanade.tachiyomi.ui.download.DownloadActivity -import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment -import eu.kanade.tachiyomi.ui.library.LibraryFragment -import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment -import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment -import eu.kanade.tachiyomi.ui.setting.SettingsActivity -import exh.ui.batchadd.BatchAddFragment -import exh.ui.lock.lockEnabled -import exh.ui.lock.notifyLockSecurity -import exh.ui.lock.showLockActivity -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.toolbar.* +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.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 +import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController +import eu.kanade.tachiyomi.ui.setting.SettingsMainController +import kotlinx.android.synthetic.main.main_activity.* import uy.kohesive.injekt.injectLazy + class MainActivity : BaseActivity() { + private lateinit var router: Router + val preferences: PreferencesHelper by injectLazy() + private var drawerArrow: DrawerArrowDrawable? = null + + private var secondaryDrawer: ViewGroup? = null + private val startScreenId by lazy { when (preferences.startScreen()) { - 1 -> R.id.nav_drawer_library 2 -> R.id.nav_drawer_recently_read 3 -> R.id.nav_drawer_recent_updates else -> R.id.nav_drawer_library } } - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) + lateinit var tabAnimator: TabsAnimator + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(when (preferences.theme()) { + 2 -> R.style.Theme_Tachiyomi_Dark + 3 -> R.style.Theme_Tachiyomi_Amoled + else -> R.style.Theme_Tachiyomi + }) + super.onCreate(savedInstanceState) // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 if (!isTaskRoot) { @@ -47,52 +64,86 @@ class MainActivity : BaseActivity() { return } - // Inflate activity_main.xml. - setContentView(R.layout.activity_main) + setContentView(R.layout.main_activity) - // Handle Toolbar - setupToolbar(toolbar, backNavigation = false) - supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp) + setSupportActionBar(toolbar) + + drawerArrow = DrawerArrowDrawable(this) + drawerArrow?.color = Color.WHITE + toolbar.navigationIcon = drawerArrow + + tabAnimator = TabsAnimator(tabs) // Set behavior of Navigation drawer nav_view.setNavigationItemSelectedListener { item -> - // Make information view invisible - empty_view.hide() - val id = item.itemId - val oldFragment = supportFragmentManager.findFragmentById(R.id.frame_container) - if (oldFragment == null || oldFragment.tag.toInt() != id) { + val currentRoot = router.backstack.firstOrNull() + if (currentRoot?.tag()?.toIntOrNull() != id) { when (id) { - R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id) - R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id) - R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id) - R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id) - R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id) + R.id.nav_drawer_library -> setRoot(LibraryController(), id) + R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) + R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) + R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) + R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id) + //TODO + // --> EH R.id.nav_drawer_batch_add -> setFragment(BatchAddFragment.newInstance(), id) - R.id.nav_drawer_downloads -> startActivity(Intent(this, DownloadActivity::class.java)) - R.id.nav_drawer_settings -> { - val intent = Intent(this, SettingsActivity::class.java) - startActivityForResult(intent, REQUEST_OPEN_SETTINGS) + // <-- EH + R.id.nav_drawer_downloads -> { + router.pushController(RouterTransaction.with(DownloadController()) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) } + R.id.nav_drawer_settings -> + router.pushController(RouterTransaction.with(SettingsMainController()) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) } } drawer.closeDrawer(GravityCompat.START) true } - if (savedState == null) { + val container = findViewById(R.id.controller_container) as ViewGroup + + router = Conductor.attachRouter(this, container, savedInstanceState) + if (!router.hasRootController()) { // Set start screen - when (intent.action) { - SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) - SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) - SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) - SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) - else -> setSelectedDrawerItem(startScreenId) + if (!handleIntentAction(intent)) { + setSelectedDrawerItem(startScreenId) + } + } + + toolbar.setNavigationOnClickListener { + if (router.backstackSize == 1) { + drawer.openDrawer(GravityCompat.START) + } else { + onBackPressed() + } + } + + router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { + override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean, + container: ViewGroup, handler: ControllerChangeHandler) { + + syncActivityViewWithController(to, from) } + override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean, + container: ViewGroup, handler: ControllerChangeHandler) { + + } + + }) + + syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) + + if (savedInstanceState == null) { // Show changelog if needed - ChangelogDialogFragment.show(this, preferences, supportFragmentManager) + if (Migrations.upgrade(preferences)) { + ChangelogDialogController().showDialog(router) + } //Show lock val lockEnabled = lockEnabled(preferences) @@ -102,79 +153,114 @@ class MainActivity : BaseActivity() { //Check lock security notifyLockSecurity(this) } - } - - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> drawer.openDrawer(GravityCompat.START) - else -> return super.onOptionsItemSelected(item) + override fun onNewIntent(intent: Intent) { + if (!handleIntentAction(intent)) { + super.onNewIntent(intent) + } + } + + private fun handleIntentAction(intent: Intent): Boolean { + when (intent.action) { + SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) + SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) + SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) + SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) + SHORTCUT_MANGA -> router.setRoot(RouterTransaction.with(MangaController(intent.extras))) + SHORTCUT_DOWNLOADS -> { + if (router.backstack.none { it.controller() is DownloadController }) { + setSelectedDrawerItem(R.id.nav_drawer_downloads) + } + } + else -> return false } return true } + override fun onDestroy() { + super.onDestroy() + nav_view?.setNavigationItemSelectedListener(null) + toolbar?.setNavigationOnClickListener(null) + } + override fun onBackPressed() { - val fragment = supportFragmentManager.findFragmentById(R.id.frame_container) + val backstackSize = router.backstackSize if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { drawer.closeDrawers() - } else if (fragment != null && fragment.tag.toInt() != startScreenId) { - if (resumed) { - setSelectedDrawerItem(startScreenId) - } - } else { + } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { + setSelectedDrawerItem(startScreenId) + } else if (backstackSize == 1 || !router.handleBack()) { super.onBackPressed() } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) { - if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) { - // If database is cleared avoid undefined behavior by recreating the stack. - TaskStackBuilder.create(this) - .addNextIntent(Intent(this, MainActivity::class.java)) - .startActivities() - } else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) { - // Delay activity recreation to avoid fragment leaks. - nav_view.post { recreate() } - } else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) { - nav_view.post { recreate() } - } else if (resultCode and SettingsActivity.FLAG_EH_RECREATE != 0) { - TaskStackBuilder.create(this) - .addNextIntent(Intent(this, MainActivity::class.java)) - .startActivities() - } - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun setSelectedDrawerItem(itemId: Int, triggerAction: Boolean = true) { - nav_view.setCheckedItem(itemId) - if (triggerAction) { + private fun setSelectedDrawerItem(itemId: Int) { + if (!isFinishing) { + nav_view.setCheckedItem(itemId) nav_view.menu.performIdentifierAction(itemId, 0) } } - private fun setFragment(fragment: Fragment, itemId: Int) { - supportFragmentManager.beginTransaction() - .replace(R.id.frame_container, fragment, "$itemId") - .commit() + private fun setRoot(controller: Controller, id: Int) { + router.setRoot(RouterTransaction.with(controller) + .popChangeHandler(FadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler()) + .tag(id.toString())) } - fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) { - if (show) empty_view.show(drawable, textResource) else empty_view.hide() + private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { + if (from is DialogController || to is DialogController) { + return + } + + val showHamburger = router.backstackSize == 1 + if (showHamburger) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + } else { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + + ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start() + + if (from is TabbedController) { + from.cleanupTabs(tabs) + } + if (to is TabbedController) { + tabAnimator.expand() + to.configureTabs(tabs) + } else { + tabAnimator.collapse() + tabs.setupWithViewPager(null) + } + + if (from is SecondaryDrawerController) { + if (secondaryDrawer != null) { + from.cleanupSecondaryDrawer(drawer) + drawer.removeView(secondaryDrawer) + secondaryDrawer = null + } + } + if (to is SecondaryDrawerController) { + secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) } + } + + if (to is NoToolbarElevationController) { + appbar.disableElevation() + } else { + appbar.enableElevation() + } } companion object { - private const val REQUEST_OPEN_SETTINGS = 200 // Shortcut actions - private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" - private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" - private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" - private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" - const val FINALIZE_MIGRATION = "finalize_migration" + const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" + const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" + const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" + const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" + const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS" + 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/main/TabsAnimator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt new file mode 100644 index 000000000..304d32f2d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt @@ -0,0 +1,106 @@ +package eu.kanade.tachiyomi.ui.main + +import android.animation.ObjectAnimator +import android.support.design.widget.TabLayout +import android.view.ViewTreeObserver +import android.view.animation.DecelerateInterpolator + +class TabsAnimator(val tabs: TabLayout) { + + /** + * The default height of the tab layout. It's unknown until the view is layout. + */ + private var tabsHeight = 0 + + /** + * Whether the last state of the tab layout is shown or hidden. + */ + private var isLastStateShown = true + + /** + * Animation used to expand and collapse the tab layout. + */ + private val animation by lazy { + ObjectAnimator.ofInt(this, "height", tabsHeight).apply { + duration = 300L + interpolator = DecelerateInterpolator() + } + } + + init { + tabs.viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + if (tabs.height > 0) { + tabs.viewTreeObserver.removeOnGlobalLayoutListener(this) + + // Save the tabs default height. + tabsHeight = tabs.height + + // Now that we know the height, set the initial height. + if (isLastStateShown) { + setHeight(tabsHeight) + } else { + setHeight(0) + } + } + } + } + ) + } + + /** + * Sets the height of the tab layout. + * + * @param newHeight The new height of the tab layout. + */ + fun setHeight(newHeight: Int) { + tabs.layoutParams.height = newHeight + tabs.requestLayout() + } + + /** + * Returns the height of the tab layout. This method is also called from the animator through + * reflection. + */ + fun getHeight(): Int { + return tabs.layoutParams.height + } + + /** + * Expands the tab layout with an animation. + */ + fun expand() { + if (isMeasured) { + if (getHeight() != tabsHeight) { + animation.setIntValues(tabsHeight) + animation.start() + } else { + animation.cancel() + } + } + isLastStateShown = true + } + + /** + * Collapse the tab layout with an animation. + */ + fun collapse() { + if (isMeasured) { + if (getHeight() != 0) { + animation.setIntValues(0) + animation.start() + } else { + animation.cancel() + } + } + isLastStateShown = false + } + + /** + * Returns whether the tab layout has a known height. + */ + private val isMeasured: Boolean + get() = tabsHeight > 0 + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt deleted file mode 100755 index efada0dc3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt +++ /dev/null @@ -1,141 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.support.graphics.drawable.VectorDrawableCompat -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager -import android.support.v4.app.FragmentPagerAdapter -import android.widget.LinearLayout -import android.widget.TextView -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment -import eu.kanade.tachiyomi.ui.manga.track.TrackFragment -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.activity_manga.* -import kotlinx.android.synthetic.main.toolbar.* -import nucleus.factory.RequiresPresenter - -@RequiresPresenter(MangaPresenter::class) -class MangaActivity : BaseRxActivity() { - - companion object { - - const val FROM_CATALOGUE_EXTRA = "from_catalogue" - const val MANGA_EXTRA = "manga" - const val FROM_LAUNCHER_EXTRA = "from_launcher" - const val INFO_FRAGMENT = 0 - const val CHAPTERS_FRAGMENT = 1 - const val TRACK_FRAGMENT = 2 - - fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent { - SharedData.put(MangaEvent(manga)) - return Intent(context, MangaActivity::class.java).apply { - putExtra(FROM_CATALOGUE_EXTRA, fromCatalogue) - putExtra(MANGA_EXTRA, manga.id) - } - } - } - - private lateinit var adapter: MangaDetailAdapter - - var fromCatalogue: Boolean = false - private set - - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) - setContentView(R.layout.activity_manga) - - val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false) - - // Remove any current manga if we are launching from launcher - if (fromLauncher) SharedData.remove(MangaEvent::class.java) - - presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) { - val id = intent.getLongExtra(MANGA_EXTRA, 0) - val dbManga = presenter.db.getManga(id).executeAsBlocking() - if (dbManga != null) { - MangaEvent(dbManga) - } else { - toast(R.string.manga_not_in_db) - finish() - return - } - }) - - setupToolbar(toolbar) - - fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false) - - adapter = MangaDetailAdapter(supportFragmentManager, this) - view_pager.offscreenPageLimit = 3 - view_pager.adapter = adapter - - tabs.setupWithViewPager(view_pager) - - if (!fromCatalogue) - view_pager.currentItem = CHAPTERS_FRAGMENT - - requestPermissionsOnMarshmallow() - } - - fun onSetManga(manga: Manga) { - setToolbarTitle(manga.title) - } - - fun setTrackingIcon(visible: Boolean) { - val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return - val drawable = if (visible) - VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null) - else null - - // I had no choice but to use reflection... - val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true } - val view = field.get(tab) as LinearLayout - val textView = view.getChildAt(1) as TextView - textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) - textView.compoundDrawablePadding = 4 - } - - private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) - : FragmentPagerAdapter(fm) { - - private var tabCount = 2 - - private val tabTitles = listOf( - R.string.manga_detail_tab, - R.string.manga_chapters_tab, - R.string.manga_tracking_tab) - .map { activity.getString(it) } - - init { - if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices()) - tabCount++ - } - - override fun getCount(): Int { - return tabCount - } - - override fun getItem(position: Int): Fragment { - when (position) { - INFO_FRAGMENT -> return MangaInfoFragment.newInstance() - CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance() - TRACK_FRAGMENT -> return TrackFragment.newInstance() - else -> throw Exception("Unknown position") - } - } - - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] - } - - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt new file mode 100644 index 000000000..562874bee --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -0,0 +1,195 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.Manifest.permission.READ_EXTERNAL_STORAGE +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.os.Build +import android.os.Bundle +import android.support.design.widget.TabLayout +import android.support.graphics.drawable.VectorDrawableCompat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.bluelinelabs.conductor.Router +import com.bluelinelabs.conductor.RouterTransaction +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.controller.RouterPagerAdapter +import eu.kanade.tachiyomi.ui.base.controller.RxController +import eu.kanade.tachiyomi.ui.base.controller.TabbedController +import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe +import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController +import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController +import eu.kanade.tachiyomi.ui.manga.track.TrackController +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.manga_controller.view.* +import rx.Subscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaController : RxController, TabbedController { + + constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply { + putLong(MANGA_EXTRA, manga?.id!!) + putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) + }) { + this.manga = manga + if (manga != null) { + source = Injekt.get().get(manga.source) + } + } + + constructor(mangaId: Long) : this( + Injekt.get().getManga(mangaId).executeAsBlocking()) + + @Suppress("unused") + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) + + var manga: Manga? = null + private set + + var source: Source? = null + private set + + private var adapter: MangaDetailAdapter? = null + + val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) + + val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() + + val mangaFavoriteRelay: PublishRelay = PublishRelay.create() + + private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() + + private var trackingIconSubscription: Subscription? = null + + override fun getTitle(): String? { + return manga?.title + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.manga_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + if (manga == null || source == null) return + + requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) + + with(view) { + adapter = MangaDetailAdapter() + view_pager.offscreenPageLimit = 3 + view_pager.adapter = adapter + + if (!fromCatalogue) + view_pager.currentItem = CHAPTERS_CONTROLLER + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type.isEnter) { + activity?.tabs?.setupWithViewPager(view?.view_pager) + trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } + } + } + + override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeEnded(handler, type) + if (manga == null || source == null) { + activity?.toast(R.string.manga_not_in_db) + router.popController(this) + } + } + + override fun configureTabs(tabs: TabLayout) { + with(tabs) { + tabGravity = TabLayout.GRAVITY_FILL + tabMode = TabLayout.MODE_FIXED + } + } + + override fun cleanupTabs(tabs: TabLayout) { + trackingIconSubscription?.unsubscribe() + setTrackingIconInternal(false) + } + + fun setTrackingIcon(visible: Boolean) { + trackingIconRelay.call(visible) + } + + private fun setTrackingIconInternal(visible: Boolean) { + val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return + val drawable = if (visible) + VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) + else null + + val view = tabField.get(tab) as LinearLayout + val textView = view.getChildAt(1) as TextView + textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) + textView.compoundDrawablePadding = if (visible) 4 else 0 + } + + private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { + + private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 + + private val tabTitles = listOf( + R.string.manga_detail_tab, + R.string.manga_chapters_tab, + R.string.manga_tracking_tab) + .map { resources!!.getString(it) } + + override fun getCount(): Int { + return tabCount + } + + override fun configureRouter(router: Router, position: Int) { + if (!router.hasRootController()) { + val controller = when (position) { + INFO_CONTROLLER -> MangaInfoController() + CHAPTERS_CONTROLLER -> ChaptersController() + TRACK_CONTROLLER -> TrackController() + else -> error("Wrong position $position") + } + router.setRoot(RouterTransaction.with(controller)) + } + } + + override fun getPageTitle(position: Int): CharSequence { + return tabTitles[position] + } + + } + + companion object { + + const val FROM_CATALOGUE_EXTRA = "from_catalogue" + const val MANGA_EXTRA = "manga" + + const val INFO_CONTROLLER = 0 + const val CHAPTERS_CONTROLLER = 1 + const val TRACK_CONTROLLER = 2 + + private val tabField = TabLayout.Tab::class.java.getDeclaredField("mView") + .apply { isAccessible = true } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt deleted file mode 100755 index ff86676da..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import eu.kanade.tachiyomi.data.database.models.Manga - -class MangaEvent(val manga: Manga) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt deleted file mode 100755 index bf9edbc0a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent -import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import uy.kohesive.injekt.injectLazy - -/** - * Presenter of [MangaActivity]. - */ -class MangaPresenter : BasePresenter() { - - /** - * Database helper. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Tracking manager. - */ - val trackManager: TrackManager by injectLazy() - - /** - * Manga associated with this instance. - */ - lateinit var manga: Manga - - var mangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Prepare a subject to communicate the chapters and info presenters for the chapter count. - SharedData.put(ChapterCountEvent()) - // Prepare a subject to communicate the chapters and info presenters for the chapter favorite. - SharedData.put(MangaFavoriteEvent()) - } - - fun setMangaEvent(event: MangaEvent) { - if (mangaSubscription.isNullOrUnsubscribed()) { - manga = event.manga - mangaSubscription = Observable.just(manga) - .subscribeLatestCache(MangaActivity::onSetManga) - } - } - -} 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 1b42ea2cf..b5ef25f1a 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,23 +6,14 @@ 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 kotlinx.android.synthetic.main.item_chapter.view.* -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols +import eu.kanade.tachiyomi.util.gone +import kotlinx.android.synthetic.main.chapters_item.view.* import java.util.* class ChapterHolder( private val view: View, - private val adapter: ChaptersAdapter) -: FlexibleViewHolder(view, adapter) { - - private val readColor = view.context.getResourceColor(android.R.attr.textColorHint) - private val unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary) - private val bookmarkedColor = view.context.getResourceColor(R.attr.colorAccent) - private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) - private val df = DateFormat.getDateInstance(DateFormat.SHORT) + private val adapter: ChaptersAdapter +) : FlexibleViewHolder(view, adapter) { init { // We need to post a Runnable to show the popup to make sure that the PopupMenu is @@ -36,23 +27,33 @@ class ChapterHolder( chapter_title.text = when (manga.displayMode) { Manga.DISPLAY_NUMBER -> { - val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble()) - context.getString(R.string.display_mode_chapter, formattedNumber) + val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) + context.getString(R.string.display_mode_chapter, number) } else -> chapter.name } // Set correct text color - chapter_title.setTextColor(if (chapter.read) readColor else unreadColor) - if (chapter.bookmark) chapter_title.setTextColor(bookmarkedColor) + chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) + if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) if (chapter.date_upload > 0) { - chapter_date.text = df.format(Date(chapter.date_upload)) - chapter_date.setTextColor(if (chapter.read) readColor else unreadColor) + chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload)) + chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) } else { chapter_date.text = "" } + //add scanlator if exists + chapter_scanlator.text = chapter.scanlator + //allow longer titles if there is no scanlator (most sources) + if (chapter_scanlator.text.isNullOrBlank()) { + chapter_title.setMaxLines(2) + chapter_scanlator.gone() + } else { + chapter_title.setMaxLines(1) + } + chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) { context.getString(R.string.chapter_progress, chapter.last_page_read + 1) } else { @@ -105,7 +106,7 @@ class ChapterHolder( // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> - adapter.menuItemListener(adapterPosition, menuItem) + adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem) true } 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 70f4a5dc1..d8afdca68 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,50 +1,57 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download - -class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), - Chapter by chapter { - - private var _status: Int = 0 - - var status: Int - get() = download?.status ?: _status - set(value) { _status = value } - - @Transient var download: Download? = null - - val isDownloaded: Boolean - get() = status == Download.DOWNLOADED - - override fun getLayoutRes(): Int { - return R.layout.item_chapter - } - - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): ChapterHolder { - return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ChapterHolder, position: Int, payloads: List?) { - holder.bind(this, manga) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is ChapterItem) { - return chapter.id!! == other.chapter.id!! - } - return false - } - - override fun hashCode(): Int { - return chapter.id!!.hashCode() - } - +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download + +class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), + Chapter by chapter { + + private var _status: Int = 0 + + var status: Int + get() = download?.status ?: _status + set(value) { _status = value } + + @Transient var download: Download? = null + + val isDownloaded: Boolean + get() = status == Download.DOWNLOADED + + override fun getLayoutRes(): Int { + 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 bindViewHolder(adapter: FlexibleAdapter<*>, + holder: ChapterHolder, + position: Int, + payloads: List?) { + + holder.bind(this, manga) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is ChapterItem) { + return chapter.id!! == other.chapter.id!! + } + return false + } + + override fun hashCode(): Int { + return chapter.id!!.hashCode() + } + } \ No newline at end of file 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 7f9cc21f1..ab72549e6 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 @@ -1,19 +1,45 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.MenuItem -import eu.davidea.flexibleadapter.FlexibleAdapter - -class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter(null, fragment, true) { - - var items: List = emptyList() - - val menuItemListener: (Int, MenuItem) -> Unit = { position, item -> - fragment.onItemMenuClick(position, item) - } - - override fun updateDataSet(items: List) { - this.items = items - super.updateDataSet(items.toList()) - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.content.Context +import android.view.MenuItem +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + +class ChaptersAdapter( + controller: ChaptersController, + context: Context +) : FlexibleAdapter(null, controller, true) { + + var items: List = emptyList() + + val menuItemListener: OnMenuItemClickListener = controller + + val readColor = context.getResourceColor(android.R.attr.textColorHint) + + val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary) + + val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) + + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) + + override fun updateDataSet(items: List) { + this.items = items + super.updateDataSet(items.toList()) + } + + fun indexOf(item: ChapterItem): Int { + return items.indexOf(item) + } + + interface OnMenuItemClickListener { + fun onMenuItemClick(position: Int, item: MenuItem) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt new file mode 100644 index 000000000..a73fe5f19 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt @@ -0,0 +1,471 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Activity +import android.content.Intent +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.DividerItemDecoration +import android.support.v7.widget.LinearLayoutManager +import android.view.* +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.view.clicks +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.getCoordinates +import eu.kanade.tachiyomi.util.snack +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.chapters_controller.view.* +import timber.log.Timber + +class ChaptersController : NucleusController(), + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ChaptersAdapter.OnMenuItemClickListener, + SetDisplayModeDialog.Listener, + SetSortingDialog.Listener, + DownloadChaptersDialog.Listener, + DeleteChaptersDialog.Listener { + + /** + * Adapter containing a list of chapters. + */ + private var adapter: ChaptersAdapter? = null + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Selected items. Used to restore selections after a rotation. + */ + private val selectedItems = mutableSetOf() + + init { + setHasOptionsMenu(true) + setOptionsMenuHidden(true) + } + + override fun createPresenter(): ChaptersPresenter { + val ctrl = parentController as MangaController + return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, + ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.chapters_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + // Init RecyclerView and adapter + adapter = ChaptersAdapter(this, view.context) + + with(view) { + recycler.adapter = adapter + 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() + + swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } + + fab.clicks().subscribeUntilDestroy { + val item = presenter.getNextUnreadChapter() + if (item != null) { + // Create animation listener + val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?) { + openChapter(item.chapter, true) + } + } + + // Get coordinates and start animation + val coordinates = fab.getCoordinates() + if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { + openChapter(item.chapter) + } + } else { + context.toast(R.string.no_next_chapter) + } + } + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + actionMode = null + } + + override fun onActivityResumed(activity: Activity) { + val view = view ?: return + + // Check if animation view is visible + if (view.reveal_view.visibility == View.VISIBLE) { + // Show the unReveal effect + val coordinates = view.fab.getCoordinates() + view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) + } + super.onActivityResumed(activity) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.chapters, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // Initialize menu items. + val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return + val menuFilterUnread = menu.findItem(R.id.action_filter_unread) + val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) + val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) + + // Set correct checkbox values. + menuFilterRead.isChecked = presenter.onlyRead() + menuFilterUnread.isChecked = presenter.onlyUnread() + menuFilterDownloaded.isChecked = presenter.onlyDownloaded() + menuFilterBookmarked.isChecked = presenter.onlyBookmarked() + + if (presenter.onlyRead()) + //Disable unread filter option if read filter is enabled. + menuFilterUnread.isEnabled = false + if (presenter.onlyUnread()) + //Disable read filter option if unread filter is enabled. + menuFilterRead.isEnabled = false + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_display_mode -> showDisplayModeDialog() + R.id.manga_download -> showDownloadDialog() + R.id.action_sorting_mode -> showSortingDialog() + R.id.action_filter_unread -> { + item.isChecked = !item.isChecked + presenter.setUnreadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_read -> { + item.isChecked = !item.isChecked + presenter.setReadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_downloaded -> { + item.isChecked = !item.isChecked + presenter.setDownloadedFilter(item.isChecked) + } + R.id.action_filter_bookmarked -> { + item.isChecked = !item.isChecked + presenter.setBookmarkedFilter(item.isChecked) + } + R.id.action_filter_empty -> { + presenter.removeFilters() + activity?.invalidateOptionsMenu() + } + R.id.action_sort -> presenter.revertSortOrder() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + fun onNextChapters(chapters: List) { + // If the list is empty, fetch chapters from source if the conditions are met + // We use presenter chapters instead because they are always unfiltered + if (presenter.chapters.isEmpty()) + initialFetchChapters() + + val adapter = adapter ?: return + adapter.updateDataSet(chapters) + + if (selectedItems.isNotEmpty()) { + adapter.clearSelection() // we need to start from a clean state, index may have changed + createActionModeIfNeeded() + selectedItems.forEach { item -> + val position = adapter.indexOf(item) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + } + } + actionMode?.invalidate() + } + + } + + private fun initialFetchChapters() { + // Only fetch if this view is from the catalog and it hasn't requested previously + if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) { + fetchChaptersFromSource() + } + } + + fun fetchChaptersFromSource() { + view?.swipe_refresh?.isRefreshing = true + presenter.fetchChaptersFromSource() + } + + fun onFetchChaptersDone() { + view?.swipe_refresh?.isRefreshing = false + } + + fun onFetchChaptersError(error: Throwable) { + view?.swipe_refresh?.isRefreshing = false + activity?.toast(error.message) + } + + fun onChapterStatusChange(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status) + } + + private fun getHolder(chapter: Chapter): ChapterHolder? { + return view?.recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + } + + fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) + if (hasAnimation) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + } + startActivity(intent) + } + + override fun onItemClick(position: Int): Boolean { + val adapter = adapter ?: return false + val item = adapter.getItem(position) ?: return false + if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + toggleSelection(position) + return true + } else { + openChapter(item.chapter) + return false + } + } + + override fun onItemLongClick(position: Int) { + createActionModeIfNeeded() + toggleSelection(position) + } + + // SELECTIONS & ACTION MODE + + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + val item = adapter.getItem(position) ?: return + adapter.toggleSelection(position) + if (adapter.isSelected(position)) { + selectedItems.add(item) + } else { + selectedItems.remove(item) + } + actionMode?.invalidate() + } + + fun getSelectedChapters(): List { + val adapter = adapter ?: return emptyList() + return adapter.selectedPositions.map { adapter.getItem(it) } + } + + fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) + } + } + + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_selection, menu) + adapter?.mode = FlexibleAdapter.MODE_MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = adapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + } + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_select_all -> selectAll() + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> showDeleteChaptersConfirmationDialog() + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + adapter?.mode = FlexibleAdapter.MODE_SINGLE + adapter?.clearSelection() + selectedItems.clear() + actionMode = null + } + + override fun onMenuItemClick(position: Int, item: MenuItem) { + val chapter = adapter?.getItem(position) ?: return + val chapters = listOf(chapter) + + when (item.itemId) { + R.id.action_download -> downloadChapters(chapters) + R.id.action_bookmark -> bookmarkChapters(chapters, true) + R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) + R.id.action_delete -> deleteChapters(chapters) + R.id.action_mark_as_read -> markAsRead(chapters) + R.id.action_mark_as_unread -> markAsUnread(chapters) + R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter) + } + } + + // SELECTION MODE ACTIONS + + fun selectAll() { + val adapter = adapter ?: return + adapter.selectAll() + selectedItems.addAll(adapter.items) + actionMode?.invalidate() + } + + fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } + + fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + } + + fun downloadChapters(chapters: List) { + val view = view + destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) + if (view != null && !presenter.manga.favorite) { + view.recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.action_add) { + presenter.addToLibrary() + } + } + } + } + + private fun showDeleteChaptersConfirmationDialog() { + DeleteChaptersDialog(this).showDialog(router) + } + + override fun deleteChapters() { + deleteChapters(getSelectedChapters()) + } + + fun markPreviousAsRead(chapter: ChapterItem) { + val adapter = adapter ?: return + val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items + val chapterPos = chapters.indexOf(chapter) + if (chapterPos != -1) { + presenter.markChaptersRead(chapters.take(chapterPos), true) + } + } + + fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + destroyActionModeIfNeeded() + presenter.bookmarkChapters(chapters, bookmarked) + } + + fun deleteChapters(chapters: List) { + destroyActionModeIfNeeded() + if (chapters.isEmpty()) return + + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(chapters) + } + + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter?.notifyDataSetChanged() + } + + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error) + } + + fun dismissDeletingDialog() { + router.popControllerWithTag(DeletingChaptersDialog.TAG) + } + + // OVERFLOW MENU DIALOGS + + private fun showDisplayModeDialog() { + val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 + SetDisplayModeDialog(this, preselected).showDialog(router) + } + + override fun setDisplayMode(id: Int) { + presenter.setDisplayMode(id) + adapter?.notifyDataSetChanged() + } + + private fun showSortingDialog() { + val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1 + SetSortingDialog(this, preselected).showDialog(router) + } + + override fun setSorting(id: Int) { + presenter.setSorting(id) + } + + private fun showDownloadDialog() { + DownloadChaptersDialog(this).showDialog(router) + } + + override fun downloadChapters(choice: Int) { + fun getUnreadChaptersSorted() = presenter.chapters + .filter { !it.read && it.status == Download.NOT_DOWNLOADED } + .distinctBy { it.name } + .sortedByDescending { it.source_order } + + // i = 0: Download 1 + // i = 1: Download 5 + // i = 2: Download 10 + // i = 3: Download unread + // i = 4: Download all + val chaptersToDownload = when (choice) { + 0 -> getUnreadChaptersSorted().take(1) + 1 -> getUnreadChaptersSorted().take(5) + 2 -> getUnreadChaptersSorted().take(10) + 3 -> presenter.chapters.filter { !it.read } + 4 -> presenter.chapters + else -> emptyList() + } + + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt deleted file mode 100755 index 2627e165c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt +++ /dev/null @@ -1,454 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Intent -import android.os.Bundle -import android.support.design.widget.Snackbar -import android.support.v4.app.DialogFragment -import android.support.v7.app.AppCompatActivity -import android.support.v7.view.ActionMode -import android.support.v7.widget.DividerItemDecoration -import android.support.v7.widget.LinearLayoutManager -import android.view.* -import com.afollestad.materialdialogs.MaterialDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.getCoordinates -import eu.kanade.tachiyomi.util.snack -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.DeletingChaptersDialog -import kotlinx.android.synthetic.main.fragment_manga_chapters.* -import nucleus.factory.RequiresPresenter -import timber.log.Timber - -@RequiresPresenter(ChaptersPresenter::class) -class ChaptersFragment : BaseRxFragment(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener { - - companion object { - /** - * Creates a new instance of this fragment. - * - * @return a new instance of [ChaptersFragment]. - */ - fun newInstance(): ChaptersFragment { - return ChaptersFragment() - } - - } - - /** - * Adapter containing a list of chapters. - */ - private lateinit var adapter: ChaptersAdapter - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_manga_chapters, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - // Init RecyclerView and adapter - adapter = ChaptersAdapter(this) - - recycler.adapter = adapter - recycler.layoutManager = LinearLayoutManager(activity) - 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() - - swipe_refresh.setOnRefreshListener { fetchChapters() } - - fab.setOnClickListener { - val item = presenter.getNextUnreadChapter() - if (item != null) { - // Create animation listener - val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { - openChapter(item.chapter, true) - } - } - - // Get coordinates and start animation - val coordinates = fab.getCoordinates() - if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else { - context.toast(R.string.no_next_chapter) - } - } - } - - override fun onResume() { - // Check if animation view is visible - if (reveal_view.visibility == View.VISIBLE) { - // Show the unReveal effect - val coordinates = fab.getCoordinates() - reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) - } - super.onResume() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.chapters, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - // Initialize menu items. - val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return - val menuFilterUnread = menu.findItem(R.id.action_filter_unread) - val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) - val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - if (presenter.onlyRead()) - //Disable unread filter option if read filter is enabled. - menuFilterUnread.isEnabled = false - if (presenter.onlyUnread()) - //Disable read filter option if unread filter is enabled. - menuFilterRead.isEnabled = false - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_display_mode -> showDisplayModeDialog() - R.id.manga_download -> showDownloadDialog() - R.id.action_sorting_mode -> showSortingDialog() - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity.supportInvalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity.supportInvalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity.supportInvalidateOptionsMenu() - } - R.id.action_sort -> presenter.revertSortOrder() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - @Suppress("UNUSED_PARAMETER") - fun onNextManga(manga: Manga) { - // Set initial values - activity.supportInvalidateOptionsMenu() - } - - fun onNextChapters(chapters: List) { - // If the list is empty, fetch chapters from source if the conditions are met - // We use presenter chapters instead because they are always unfiltered - if (presenter.chapters.isEmpty()) - initialFetchChapters() - - destroyActionModeIfNeeded() - adapter.updateDataSet(chapters) - } - - private fun initialFetchChapters() { - // Only fetch if this view is from the catalog and it hasn't requested previously - if (isCatalogueManga && !presenter.hasRequested) { - fetchChapters() - } - } - - fun fetchChapters() { - swipe_refresh.isRefreshing = true - presenter.fetchChaptersFromSource() - } - - fun onFetchChaptersDone() { - swipe_refresh.isRefreshing = false - } - - fun onFetchChaptersError(error: Throwable) { - swipe_refresh.isRefreshing = false - context.toast(error.message) - } - - val isCatalogueManga: Boolean - get() = (activity as MangaActivity).fromCatalogue - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - private fun showDisplayModeDialog() { - // Get available modes, ids and the selected mode - val modes = intArrayOf(R.string.show_title, R.string.show_chapter_number) - val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) - val selectedIndex = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 - - MaterialDialog.Builder(activity) - .title(R.string.action_display_mode) - .items(modes.map { getString(it) }) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> - // Save the new display mode - presenter.setDisplayMode(itemView.id) - // Refresh ui - adapter.notifyItemRangeChanged(0, adapter.itemCount) - true - } - .show() - } - - private fun showSortingDialog() { - // Get available modes, ids and the selected mode - val modes = intArrayOf(R.string.sort_by_source, R.string.sort_by_number) - val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER) - val selectedIndex = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1 - - MaterialDialog.Builder(activity) - .title(R.string.sorting_mode) - .items(modes.map { getString(it) }) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> - // Save the new sorting mode - presenter.setSorting(itemView.id) - true - } - .show() - } - - private fun showDownloadDialog() { - // Get available modes - val modes = intArrayOf(R.string.download_1, R.string.download_5, R.string.download_10, - R.string.download_unread, R.string.download_all) - - MaterialDialog.Builder(activity) - .title(R.string.manga_download) - .negativeText(android.R.string.cancel) - .items(modes.map { getString(it) }) - .itemsCallback { _, _, i, _ -> - - fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - // i = 0: Download 1 - // i = 1: Download 5 - // i = 2: Download 10 - // i = 3: Download unread - // i = 4: Download all - val chaptersToDownload = when (i) { - 0 -> getUnreadChaptersSorted().take(1) - 1 -> getUnreadChaptersSorted().take(5) - 2 -> getUnreadChaptersSorted().take(10) - 3 -> presenter.chapters.filter { !it.read } - 4 -> presenter.chapters - else -> emptyList() - } - - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } - .show() - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_selection, menu) - adapter.mode = FlexibleAdapter.MODE_MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> { - MaterialDialog.Builder(activity) - .content(R.string.confirm_delete_chapters) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { _, _ -> deleteChapters(getSelectedChapters()) } - .show() - } - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - adapter.mode = FlexibleAdapter.MODE_SINGLE - adapter.clearSelection() - actionMode = null - } - - fun getSelectedChapters(): List { - return adapter.selectedPositions.map { adapter.getItem(it) } - } - - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - fun selectAll() { - adapter.selectAll() - setContextTitle(adapter.selectedItemCount) - } - - fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - } - - fun markPreviousAsRead(chapter: ChapterItem) { - val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = chapters.indexOf(chapter) - if (chapterPos != -1) { - presenter.markChaptersRead(chapters.take(chapterPos), true) - } - } - - fun downloadChapters(chapters: List) { - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - if (!presenter.manga.favorite){ - recycler.snack(getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - } - } - } - - fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - destroyActionModeIfNeeded() - presenter.bookmarkChapters(chapters, bookmarked) - } - - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) - presenter.deleteChapters(chapters) - } - - fun onChaptersDeleted() { - dismissDeletingDialog() - adapter.notifyItemRangeChanged(0, adapter.itemCount) - } - - fun onChaptersDeletedError(error: Throwable) { - dismissDeletingDialog() - Timber.e(error) - } - - fun dismissDeletingDialog() { - (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment) - ?.dismissAllowingStateLoss() - } - - override fun onItemClick(position: Int): Boolean { - val item = adapter.getItem(position) ?: return false - if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item.chapter) - return false - } - } - - override fun onItemLongClick(position: Int) { - if (actionMode == null) - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - - toggleSelection(position) - } - - fun onItemMenuClick(position: Int, item: MenuItem) { - val chapter = adapter.getItem(position)?.let { listOf(it) } ?: return - - when (item.itemId) { - R.id.action_download -> downloadChapters(chapter) - R.id.action_bookmark -> bookmarkChapters(chapter, true) - R.id.action_remove_bookmark -> bookmarkChapters(chapter, false) - R.id.action_delete -> deleteChapters(chapter) - R.id.action_mark_as_read -> markAsRead(chapter) - R.id.action_mark_as_unread -> markAsUnread(chapter) - R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter[0]) - } - } - - private fun toggleSelection(position: Int) { - adapter.toggleSelection(position) - - val count = adapter.selectedItemCount - if (count == 0) { - actionMode?.finish() - } else { - setContextTitle(count) - actionMode?.invalidate() - } - } - - private fun setContextTitle(count: Int) { - actionMode?.title = getString(R.string.label_selected, count) - } -} 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 be294a97e..71283739d 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 @@ -1,446 +1,416 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.os.Bundle -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.DownloadService -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.MangaEvent -import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent -import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.syncChaptersWithSource -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import uy.kohesive.injekt.injectLazy - -/** - * Presenter of [ChaptersFragment]. - */ -class ChaptersPresenter : BasePresenter() { - - /** - * Database helper. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Source manager. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Preferences. - */ - val preferences: PreferencesHelper by injectLazy() - - /** - * Downloads manager. - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Active manga. - */ - lateinit var manga: Manga - private set - - /** - * Source of the manga. - */ - lateinit var source: Source - private set - - /** - * List of chapters of the manga. It's always unfiltered and unsorted. - */ - var chapters: List = emptyList() - private set - - /** - * Subject of list of chapters to allow updating the view without going to DB. - */ - val chaptersRelay: PublishRelay> - by lazy { PublishRelay.create>() } - - /** - * Whether the chapter list has been requested to the source. - */ - var hasRequested = false - private set - - /** - * Subscription to retrieve the new list of chapters from the source. - */ - private var fetchChaptersSubscription: Subscription? = null - - /** - * Subscription to observe download status changes. - */ - private var observeDownloadsSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Find the active manga from the shared data or return. - manga = SharedData.get(MangaEvent::class.java)?.manga ?: return - source = sourceManager.get(manga.source)!! - Observable.just(manga) - .subscribeLatestCache(ChaptersFragment::onNextManga) - - // Prepare the relay. - chaptersRelay.flatMap { applyChapterFilters(it) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(ChaptersFragment::onNextChapters, - { _, error -> Timber.e(error) }) - - // Add the subscription that retrieves the chapters from the database, keeps subscribed to - // changes, and sends the list of chapters to the relay. - add(db.getChapters(manga).asRxObservable() - .map { chapters -> - // Convert every chapter to a model. - chapters.map { it.toModel() } - } - .doOnNext { chapters -> - // Find downloaded chapters - setDownloadedChapters(chapters) - - // Store the last emission - this.chapters = chapters - - // Listen for download status changes - observeDownloads() - - // Emit the number of chapters to the info tab. - SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size) - } - .subscribe { chaptersRelay.call(it) }) - } - - private fun observeDownloads() { - observeDownloadsSubscription?.let { remove(it) } - observeDownloadsSubscription = downloadManager.queue.getStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .filter { download -> download.manga.id == manga.id } - .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(ChaptersFragment::onChapterStatusChange, - { _, error -> Timber.e(error) }) - } - - /** - * Converts a chapter from the database to an extended model, allowing to store new fields. - */ - private fun Chapter.toModel(): ChapterItem { - // Create the model object. - val model = ChapterItem(this, manga) - - // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == id } - - if (download != null) { - // If there's an active download, assign it. - model.download = download - } - return model - } - - /** - * Finds and assigns the list of downloaded chapters. - * - * @param chapters the list of chapter from the database. - */ - private fun setDownloadedChapters(chapters: List) { - val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return - val cached = mutableMapOf() - files.mapNotNull { it.name } - .mapNotNull { name -> chapters.find { - name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) } - } } - .forEach { it.status = Download.DOWNLOADED } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - - if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onFetchChaptersDone() - }, ChaptersFragment::onFetchChaptersError) - } - - /** - * Updates the UI after applying the filters. - */ - private fun refreshChapters() { - chaptersRelay.call(chapters) - } - - /** - * Applies the view filters to the list of chapters obtained from the database. - * @param chapters the list of chapters from the database - * @return an observable of the list of chapters filtered and sorted. - */ - private fun applyChapterFilters(chapters: List): Observable> { - var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) - if (onlyUnread()) { - observable = observable.filter { !it.read } - } - else if (onlyRead()) { - observable = observable.filter { it.read } - } - if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded } - } - if (onlyBookmarked()) { - observable = observable.filter { it.bookmark } - } - val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { - Manga.SORTING_SOURCE -> when (sortDescending()) { - true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } - false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } - } - Manga.SORTING_NUMBER -> when (sortDescending()) { - true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } - false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } - } - else -> throw NotImplementedError("Unimplemented sorting method") - } - return observable.toSortedList(sortFunction) - } - - /** - * Called when a download for the active manga changes status. - * @param download the download whose status changed. - */ - fun onDownloadStatusChange(download: Download) { - // Assign the download to the model object. - if (download.status == Download.QUEUE) { - chapters.find { it.id == download.chapter.id }?.let { - if (it.download == null) { - it.download = download - } - } - } - - // Force UI update if downloaded filter active and download finished. - if (onlyDownloaded() && download.status == Download.DOWNLOADED) - refreshChapters() - } - - /** - * Returns the next unread chapter or null if everything is read. - */ - fun getNextUnreadChapter(): ChapterItem? { - return chapters.sortedByDescending { it.source_order }.find { !it.read } - } - - /** - * Mark the selected chapter list as read/unread. - * @param selectedChapters the list of selected chapters. - * @param read whether to mark chapters as read or unread. - */ - fun markChaptersRead(selectedChapters: List, read: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.read = read - if (!read) { - chapter.last_page_read = 0 - } - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Downloads the given list of chapters with the manager. - * @param chapters the list of chapters to download. - */ - fun downloadChapters(chapters: List) { - DownloadService.start(context) - downloadManager.downloadChapters(manga, chapters) - } - - /** - * Bookmarks the given list of chapters. - * @param selectedChapters the list of chapters to bookmark. - */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.bookmark = bookmarked - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Deletes the given list of chapter. - * @param chapters the list of chapters to delete. - */ - fun deleteChapters(chapters: List) { - Observable.from(chapters) - .doOnNext { deleteChapter(it) } - .toList() - .doOnNext { if (onlyDownloaded()) refreshChapters() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onChaptersDeleted() - }, ChaptersFragment::onChaptersDeletedError) - } - - /** - * Deletes a chapter from disk. This method is called in a background thread. - * @param chapter the chapter to delete. - */ - private fun deleteChapter(chapter: ChapterItem) { - downloadManager.queue.remove(chapter) - downloadManager.deleteChapter(source, manga, chapter) - chapter.status = Download.NOT_DOWNLOADED - chapter.download = null - } - - /** - * Reverses the sorting and requests an UI update. - */ - fun revertSortOrder() { - manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyUnread whether to display only unread chapters or all chapters. - */ - fun setUnreadFilter(onlyUnread: Boolean) { - manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyRead whether to display only read chapters or all chapters. - */ - fun setReadFilter(onlyRead: Boolean) { - manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the download filter and requests an UI update. - * @param onlyDownloaded whether to display only downloaded chapters or all chapters. - */ - fun setDownloadedFilter(onlyDownloaded: Boolean) { - manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the bookmark filter and requests an UI update. - * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. - */ - fun setBookmarkedFilter(onlyBookmarked: Boolean) { - manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - SharedData.get(MangaFavoriteEvent::class.java)?.call(true) - } - - /** - * Sets the active display mode. - * @param mode the mode to set. - */ - fun setDisplayMode(mode: Int) { - manga.displayMode = mode - db.updateFlags(manga).executeAsBlocking() - } - - /** - * Sets the sorting method and requests an UI update. - * @param sort the sorting mode. - */ - fun setSorting(sort: Int) { - manga.sorting = sort - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyDownloaded(): Boolean { - return manga.downloadedFilter == Manga.SHOW_DOWNLOADED - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyBookmarked(): Boolean { - return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED - } - - /** - * Whether the display only unread filter is enabled. - */ - fun onlyUnread(): Boolean { - return manga.readFilter == Manga.SHOW_UNREAD - } - - /** - * Whether the display only read filter is enabled. - */ - fun onlyRead(): Boolean { - return manga.readFilter == Manga.SHOW_READ - } - - /** - * Whether the sorting method is descending or ascending. - */ - fun sortDescending(): Boolean { - return manga.sortDescending() - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import eu.kanade.tachiyomi.util.syncChaptersWithSource +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of [ChaptersController]. + */ +class ChaptersPresenter( + val manga: Manga, + val source: Source, + private val chapterCountRelay: BehaviorRelay, + private val mangaFavoriteRelay: PublishRelay, + val preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + /** + * List of chapters of the manga. It's always unfiltered and unsorted. + */ + var chapters: List = emptyList() + private set + + /** + * Subject of list of chapters to allow updating the view without going to DB. + */ + val chaptersRelay: PublishRelay> + by lazy { PublishRelay.create>() } + + /** + * Whether the chapter list has been requested to the source. + */ + var hasRequested = false + private set + + /** + * Subscription to retrieve the new list of chapters from the source. + */ + private var fetchChaptersSubscription: Subscription? = null + + /** + * Subscription to observe download status changes. + */ + private var observeDownloadsSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Prepare the relay. + chaptersRelay.flatMap { applyChapterFilters(it) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(ChaptersController::onNextChapters, + { _, error -> Timber.e(error) }) + + // Add the subscription that retrieves the chapters from the database, keeps subscribed to + // changes, and sends the list of chapters to the relay. + add(db.getChapters(manga).asRxObservable() + .map { chapters -> + // Convert every chapter to a model. + chapters.map { it.toModel() } + } + .doOnNext { chapters -> + // Find downloaded chapters + setDownloadedChapters(chapters) + + // Store the last emission + this.chapters = chapters + + // Listen for download status changes + observeDownloads() + + // Emit the number of chapters to the info tab. + chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number + ?: 0f) + } + .subscribe { chaptersRelay.call(it) }) + } + + private fun observeDownloads() { + observeDownloadsSubscription?.let { remove(it) } + observeDownloadsSubscription = downloadManager.queue.getStatusObservable() + .observeOn(AndroidSchedulers.mainThread()) + .filter { download -> download.manga.id == manga.id } + .doOnNext { onDownloadStatusChange(it) } + .subscribeLatestCache(ChaptersController::onChapterStatusChange, + { _, error -> Timber.e(error) }) + } + + /** + * Converts a chapter from the database to an extended model, allowing to store new fields. + */ + private fun Chapter.toModel(): ChapterItem { + // Create the model object. + val model = ChapterItem(this, manga) + + // Find an active download for this chapter. + val download = downloadManager.queue.find { it.chapter.id == id } + + if (download != null) { + // If there's an active download, assign it. + model.download = download + } + return model + } + + /** + * Finds and assigns the list of downloaded chapters. + * + * @param chapters the list of chapter from the database. + */ + private fun setDownloadedChapters(chapters: List) { + val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return + val cached = mutableMapOf() + files.mapNotNull { it.name } + .mapNotNull { name -> chapters.find { + name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) } + } } + .forEach { it.status = Download.DOWNLOADED } + } + + /** + * Requests an updated list of chapters from the source. + */ + fun fetchChaptersFromSource() { + hasRequested = true + + if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return + fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } + .subscribeOn(Schedulers.io()) + .map { syncChaptersWithSource(db, it, manga, source) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> + view.onFetchChaptersDone() + }, ChaptersController::onFetchChaptersError) + } + + /** + * Updates the UI after applying the filters. + */ + private fun refreshChapters() { + chaptersRelay.call(chapters) + } + + /** + * Applies the view filters to the list of chapters obtained from the database. + * @param chapters the list of chapters from the database + * @return an observable of the list of chapters filtered and sorted. + */ + private fun applyChapterFilters(chapters: List): Observable> { + var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) + if (onlyUnread()) { + observable = observable.filter { !it.read } + } + else if (onlyRead()) { + observable = observable.filter { it.read } + } + if (onlyDownloaded()) { + observable = observable.filter { it.isDownloaded } + } + if (onlyBookmarked()) { + observable = observable.filter { it.bookmark } + } + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { + Manga.SORTING_SOURCE -> when (sortDescending()) { + true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } + false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } + } + Manga.SORTING_NUMBER -> when (sortDescending()) { + true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } + false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } + } + else -> throw NotImplementedError("Unimplemented sorting method") + } + return observable.toSortedList(sortFunction) + } + + /** + * Called when a download for the active manga changes status. + * @param download the download whose status changed. + */ + fun onDownloadStatusChange(download: Download) { + // Assign the download to the model object. + if (download.status == Download.QUEUE) { + chapters.find { it.id == download.chapter.id }?.let { + if (it.download == null) { + it.download = download + } + } + } + + // Force UI update if downloaded filter active and download finished. + if (onlyDownloaded() && download.status == Download.DOWNLOADED) + refreshChapters() + } + + /** + * Returns the next unread chapter or null if everything is read. + */ + fun getNextUnreadChapter(): ChapterItem? { + return chapters.sortedByDescending { it.source_order }.find { !it.read } + } + + /** + * Mark the selected chapter list as read/unread. + * @param selectedChapters the list of selected chapters. + * @param read whether to mark chapters as read or unread. + */ + fun markChaptersRead(selectedChapters: List, read: Boolean) { + Observable.from(selectedChapters) + .doOnNext { chapter -> + chapter.read = read + if (!read) { + chapter.last_page_read = 0 + } + } + .toList() + .flatMap { db.updateChaptersProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Downloads the given list of chapters with the manager. + * @param chapters the list of chapters to download. + */ + fun downloadChapters(chapters: List) { + DownloadService.start(context) + downloadManager.downloadChapters(manga, chapters) + } + + /** + * Bookmarks the given list of chapters. + * @param selectedChapters the list of chapters to bookmark. + */ + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + Observable.from(selectedChapters) + .doOnNext { chapter -> + chapter.bookmark = bookmarked + } + .toList() + .flatMap { db.updateChaptersProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Deletes the given list of chapter. + * @param chapters the list of chapters to delete. + */ + fun deleteChapters(chapters: List) { + Observable.from(chapters) + .doOnNext { deleteChapter(it) } + .toList() + .doOnNext { if (onlyDownloaded()) refreshChapters() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> + view.onChaptersDeleted() + }, ChaptersController::onChaptersDeletedError) + } + + /** + * Deletes a chapter from disk. This method is called in a background thread. + * @param chapter the chapter to delete. + */ + private fun deleteChapter(chapter: ChapterItem) { + downloadManager.queue.remove(chapter) + downloadManager.deleteChapter(source, manga, chapter) + chapter.status = Download.NOT_DOWNLOADED + chapter.download = null + } + + /** + * Reverses the sorting and requests an UI update. + */ + fun revertSortOrder() { + manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the read filter and requests an UI update. + * @param onlyUnread whether to display only unread chapters or all chapters. + */ + fun setUnreadFilter(onlyUnread: Boolean) { + manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the read filter and requests an UI update. + * @param onlyRead whether to display only read chapters or all chapters. + */ + fun setReadFilter(onlyRead: Boolean) { + manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the download filter and requests an UI update. + * @param onlyDownloaded whether to display only downloaded chapters or all chapters. + */ + fun setDownloadedFilter(onlyDownloaded: Boolean) { + manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the bookmark filter and requests an UI update. + * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. + */ + fun setBookmarkedFilter(onlyBookmarked: Boolean) { + manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Removes all filters and requests an UI update. + */ + fun removeFilters() { + manga.readFilter = Manga.SHOW_ALL + manga.downloadedFilter = Manga.SHOW_ALL + manga.bookmarkedFilter = Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Adds manga to library + */ + fun addToLibrary() { + mangaFavoriteRelay.call(true) + } + + /** + * Sets the active display mode. + * @param mode the mode to set. + */ + fun setDisplayMode(mode: Int) { + manga.displayMode = mode + db.updateFlags(manga).executeAsBlocking() + } + + /** + * Sets the sorting method and requests an UI update. + * @param sort the sorting mode. + */ + fun setSorting(sort: Int) { + manga.sorting = sort + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyDownloaded(): Boolean { + return manga.downloadedFilter == Manga.SHOW_DOWNLOADED + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyBookmarked(): Boolean { + return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED + } + + /** + * Whether the display only unread filter is enabled. + */ + fun onlyUnread(): Boolean { + return manga.readFilter == Manga.SHOW_UNREAD + } + + /** + * Whether the display only read filter is enabled. + */ + fun onlyRead(): Boolean { + return manga.readFilter == Manga.SHOW_READ + } + + /** + * Whether the sorting method is descending or ascending. + */ + fun sortDescending(): Boolean { + return manga.sortDescending() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt new file mode 100644 index 000000000..a269fe085 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt @@ -0,0 +1,32 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : DeleteChaptersDialog.Listener { + + constructor(target: T) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .content(R.string.confirm_delete_chapters) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + (targetController as? Listener)?.deleteChapters() + } + .show() + } + + interface Listener { + fun deleteChapters() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt new file mode 100644 index 000000000..fcfd6b9ad --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Router +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) { + + companion object { + const val TAG = "deleting_dialog" + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .progress(true, 0) + .content(R.string.deleting) + .build() + } + + override fun showDialog(router: Router) { + showDialog(router, TAG) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt new file mode 100644 index 000000000..c54797a1f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DownloadChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : DownloadChaptersDialog.Listener { + + constructor(target: T) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + + val choices = intArrayOf( + R.string.download_1, + R.string.download_5, + R.string.download_10, + R.string.download_unread, + R.string.download_all + ).map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.manga_download) + .negativeText(android.R.string.cancel) + .items(choices) + .itemsCallback { _, _, position, _ -> + (targetController as? Listener)?.downloadChapters(position) + } + .build() + } + + interface Listener { + fun downloadChapters(choice: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt new file mode 100644 index 000000000..608742b74 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class SetDisplayModeDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SetDisplayModeDialog.Listener { + + private val selectedIndex = args.getInt("selected", -1) + + constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { + putInt("selected", selectedIndex) + }) { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) + val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.action_display_mode) + .items(choices) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> + (targetController as? Listener)?.setDisplayMode(itemView.id) + true + } + .build() + } + + interface Listener { + fun setDisplayMode(id: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt new file mode 100644 index 000000000..c6baca5b9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class SetSortingDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SetSortingDialog.Listener { + + private val selectedIndex = args.getInt("selected", -1) + + constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { + putInt("selected", selectedIndex) + }) { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER) + val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.sorting_mode) + .items(choices) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> + (targetController as? Listener)?.setSorting(itemView.id) + true + } + .build() + } + + interface Listener { + fun setSorting(id: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt deleted file mode 100755 index d307941bc..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import rx.Observable -import rx.subjects.BehaviorSubject - -class ChapterCountEvent { - - private val subject = BehaviorSubject.create() - - val observable: Observable - get() = subject - - fun emit(count: Int) { - subject.onNext(count) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt deleted file mode 100755 index 75beb742c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import com.jakewharton.rxrelay.PublishRelay -import rx.Observable - -class MangaFavoriteEvent { - - private val subject = PublishRelay.create() - - val observable: Observable - get() = subject - - fun call(favorite: Boolean) { - subject.call(favorite) - } -} 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 new file mode 100644 index 000000000..d1febd2d5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -0,0 +1,402 @@ +package eu.kanade.tachiyomi.ui.manga.info + +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.support.customtabs.CustomTabsIntent +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.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.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.NucleusController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.main.MainActivity +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 + +/** + * Fragment that shows manga information. + * Uses R.layout.manga_info_controller. + * UI related actions should be called from here. + */ +class MangaInfoController : NucleusController(), + ChangeMangaCategoriesDialog.Listener { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + init { + setHasOptionsMenu(true) + setOptionsMenuHidden(true) + } + + override fun createPresenter(): MangaInfoPresenter { + val ctrl = parentController as MangaController + return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, + ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.manga_info_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + // Set onclickListener to toggle favorite when FAB clicked. + fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } + + // Set SwipeRefresh to refresh manga data. + swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } + } + + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.manga_info, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_open_in_browser -> openInBrowser() + R.id.action_share -> shareManga() + R.id.action_add_to_home_screen -> addToHomeScreen() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Check if manga is initialized. + * If true update view with manga information, + * if false fetch manga information + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun onNextManga(manga: Manga, source: Source) { + if (manga.initialized) { + // Update view. + setMangaInfo(manga, source) + } else { + // Initialize manga. + fetchMangaFromSource() + } + } + + /** + * Update the view with manga information. + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + private fun setMangaInfo(manga: Manga, source: Source?) { + val view = view ?: return + with(view) { + // Update artist TextView. + manga_artist.text = manga.artist + + // Update author TextView. + manga_author.text = manga.author + + // If manga source is known update source TextView. + if (source != null) { + manga_source.text = source.toString() + } + + // Update genres TextView. + manga_genres.text = manga.genre + + // Update status TextView. + manga_status.setText(when (manga.status) { + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed + else -> R.string.unknown + }) + + // Update description TextView. + manga_summary.text = manga.description + + // Set the favorite drawable to the correct one. + setFavoriteDrawable(manga.favorite) + + // Set cover if it wasn't already. + if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { + Glide.with(context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .into(manga_cover) + + if (backdrop != null) { + Glide.with(context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .into(backdrop) + } + } + } + } + + /** + * Update chapter count TextView. + * + * @param count number of chapters. + */ + fun setChapterCount(count: Float) { + view?.manga_chapters?.text = DecimalFormat("#.#").format(count) + } + + /** + * Toggles the favorite status and asks for confirmation to delete downloaded chapters. + */ + fun toggleFavorite() { + val view = view + + val isNowFavorite = presenter.toggleFavorite() + if (view != null && !isNowFavorite && presenter.hasDownloads()) { + view.snack(view.context.getString(R.string.delete_downloads_for_manga)) { + setAction(R.string.action_delete) { + presenter.deleteDownloads() + } + } + } + } + + /** + * Open the manga in browser. + */ + fun openInBrowser() { + val context = view?.context ?: return + val source = presenter.source as? HttpSource ?: return + + try { + val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString()) + val intent = CustomTabsIntent.Builder() + .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) + .build() + intent.launchUrl(activity, url) + } catch (e: Exception) { + context.toast(e.message) + } + } + + /** + * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. + */ + private fun shareManga() { + val context = view?.context ?: return + + val source = presenter.source as? HttpSource ?: return + try { + val url = source.mangaDetailsRequest(presenter.manga).url().toString() + val title = presenter.manga.title + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_text, title, url)) + } + startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + /** + * Update FAB with correct drawable. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun setFavoriteDrawable(isFavorite: Boolean) { + // Set the Favorite drawable to the correct one. + // Border drawable if false, filled drawable if true. + view?.fab_favorite?.setImageResource(if (isFavorite) + R.drawable.ic_bookmark_white_24dp + else + R.drawable.ic_bookmark_border_white_24dp) + } + + /** + * Start fetching manga information from source. + */ + private fun fetchMangaFromSource() { + setRefreshing(true) + // Call presenter and start fetching manga information + presenter.fetchMangaFromSource() + } + + + /** + * Update swipe refresh to stop showing refresh in progress spinner. + */ + fun onFetchMangaDone() { + setRefreshing(false) + } + + /** + * Update swipe refresh to start showing refresh in progress spinner. + */ + fun onFetchMangaError() { + setRefreshing(false) + } + + /** + * Set swipe refresh status. + * + * @param value whether it should be refreshing or not. + */ + private fun setRefreshing(value: Boolean) { + view?.swipe_refresh?.isRefreshing = value + } + + /** + * Called when the fab is clicked. + */ + private fun onFabClick() { + val manga = presenter.manga + toggleFavorite() + if (manga.favorite) { + val categories = presenter.getCategories() + val 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() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.moveMangaToCategories(manga, categories) + } + + /** + * Add the manga to the home screen + */ + fun addToHomeScreen() { + val activity = activity ?: return + val mangaControllerArgs = parentController?.args ?: return + + 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) + + //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()) + + reshapeIconBitmap(addIntent, + Glide.with(activity).load(presenter.manga).asBitmap()) + }) + .negativeText(android.R.string.cancel) + .show() + + 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 + } + } + + 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) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt deleted file mode 100755 index 5492e2456..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt +++ /dev/null @@ -1,393 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.os.Bundle -import android.support.customtabs.CustomTabsIntent -import android.view.* -import android.widget.Toast -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 eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -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.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -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.fragment_manga_info.* -import nucleus.factory.RequiresPresenter -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy - -/** - * Fragment that shows manga information. - * Uses R.layout.fragment_manga_info. - * UI related actions should be called from here. - */ -@RequiresPresenter(MangaInfoPresenter::class) -class MangaInfoFragment : BaseRxFragment() { - - companion object { - /** - * Create new instance of MangaInfoFragment. - * - * @return MangaInfoFragment. - */ - fun newInstance(): MangaInfoFragment { - return MangaInfoFragment() - } - - } - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_manga_info, container, false) - } - - override fun onViewCreated(view: View?, savedState: Bundle?) { - // Set onclickListener to toggle favorite when FAB clicked. - fab_favorite.setOnClickListener { - if(!presenter.manga.favorite) { - val defaultCategory = presenter.getCategories().find { it.id == preferences.defaultCategory()} - if(defaultCategory == null) { - onFabClick() - } else { - toggleFavorite() - presenter.moveMangaToCategory(defaultCategory, presenter.manga) - } - } else { - toggleFavorite() - } - } - - // Set SwipeRefresh to refresh manga data. - swipe_refresh.setOnRefreshListener { fetchMangaFromSource() } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.manga_info, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_open_in_browser -> openInBrowser() - R.id.action_share -> shareManga() - R.id.action_add_to_home_screen -> addToHomeScreen() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextManga(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source) - } else { - // Initialize manga. - fetchMangaFromSource() - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - // Update artist TextView. - manga_artist.text = manga.artist - - // Update author TextView. - manga_author.text = manga.author - - // If manga source is known update source TextView. - if (source != null) { - manga_source.text = source.toString() - } - - // Update genres TextView. - manga_genres.text = manga.genre - - // Update status TextView. - manga_status.setText(when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - }) - - // Update description TextView. - manga_summary.text = manga.description - - // Set the favorite drawable to the correct one. - setFavoriteDrawable(manga.favorite) - - // Set cover if it wasn't already. - if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(this) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(manga_cover) - - Glide.with(this) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(backdrop) - } - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Int) { - manga_chapters.text = count.toString() - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - fun toggleFavorite() { - if (!isAdded) return - - val isNowFavorite = presenter.toggleFavorite() - if (!isNowFavorite && presenter.hasDownloads()) { - view!!.snack(getString(R.string.delete_downloads_for_manga)) { - setAction(R.string.action_delete) { - presenter.deleteDownloads() - } - } - } - } - - /** - * Open the manga in browser. - */ - fun openInBrowser() { - if (!isAdded) return - - val source = presenter.source as? HttpSource ?: return - try { - val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString()) - val intent = CustomTabsIntent.Builder() - .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) - .build() - intent.launchUrl(activity, url) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - private fun shareManga() { - if (!isAdded) return - - val source = presenter.source as? HttpSource ?: return - try { - val url = source.mangaDetailsRequest(presenter.manga).url().toString() - val sharingIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text, presenter.manga.title, url)) - } - startActivity(Intent.createChooser(sharingIntent, getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Add the manga to the home screen - */ - fun addToHomeScreen() { - if (!isAdded) return - - val shortcutIntent = activity.intent - shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaActivity.FROM_LAUNCHER_EXTRA, true) - - val addIntent = Intent() - addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) - .action = "com.android.launcher.action.INSTALL_SHORTCUT" - - //Set shortcut title - MaterialDialog.Builder(activity) - .title(R.string.shortcut_title) - .input("", presenter.manga.title, { md, text -> - //Set shortcut title - addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString()) - - reshapeIconBitmap(addIntent, - Glide.with(context).load(presenter.manga).asBitmap()) - }) - .negativeText(android.R.string.cancel) - .onNegative { materialDialog, dialogAction -> materialDialog.cancel() } - .show() - } - - fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest) { - 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() - } - - MaterialDialog.Builder(activity) - .title(R.string.icon_shape) - .negativeText(android.R.string.cancel) - .items(modes.map { getString(it) }) - .itemsCallback { dialog, view, i, charSequence -> - Observable.fromCallable { - // i = 0: Circular icon - // i = 1: Rounded icon - // i = 2: Square icon - // i = 3: Star icon (because boredom) - when (i) { - 0 -> request.transform(CropCircleTransformation(context)).toIcon() - 1 -> request.transform(RoundedCornersTransformation(context, 5, 0)).toIcon() - 2 -> request.transform(CropSquareTransformation(context)).toIcon() - 3 -> request.transform(CenterCrop(context), MaskTransformation(context, R.drawable.mask_star)).toIcon() - else -> null - } - }.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ if (it != null) createShortcut(addIntent, it) }, - { context.toast(R.string.icon_creation_fail) }) - }.show() - } - - fun createShortcut(addIntent: Intent, icon: Bitmap) { - //Send shortcut intent - addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon) - context.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) - } - - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteDrawable(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fab_favorite.setImageResource(if (isFavorite) - R.drawable.ic_bookmark_white_24dp - else - R.drawable.ic_bookmark_border_white_24dp) - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaFromSource() { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource() - } - - - /** - * Update swipe refresh to stop showing refresh in progress spinner. - */ - fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - fun onFetchMangaError() { - setRefreshing(false) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - private fun setRefreshing(value: Boolean) { - swipe_refresh.isRefreshing = value - } - - /** - * Called when the fab is clicked. - */ - private fun onFabClick() { - val categories = presenter.getCategories() - - MaterialDialog.Builder(activity) - .title(R.string.action_move_category) - .items(categories.map { it.name }) - .itemsCallbackMultiChoice(presenter.getMangaCategoryIds(presenter.manga)) { dialog, position, text -> - if (position.contains(0) && position.count() > 1) { - dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray()) - Toast.makeText(dialog.context, R.string.invalid_combination, Toast.LENGTH_SHORT).show() - } - - true - } - .alwaysCallMultiChoiceCallback() - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog, _ -> - val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList() - - if(!selectedCategories.isEmpty()) { - if(!presenter.manga.favorite) { - toggleFavorite() - } - presenter.moveMangaToCategories(selectedCategories.filter { it.id != 0}, presenter.manga) - } else { - toggleFavorite() - } - } - .build() - .show() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index 846f8fd88..9ffb1023d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.ui.manga.info import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -8,53 +10,29 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.MangaEvent -import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.toast import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** * Presenter of MangaInfoFragment. * Contains information and data for fragment. * Observable updates should be called from here. */ -class MangaInfoPresenter : BasePresenter() { - - /** - * Active manga. - */ - lateinit var manga: Manga - private set - - /** - * Source of the manga. - */ - lateinit var source: Source - private set - - /** - * Used to connect to database. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Used to connect to different manga sources. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Used to connect to cache. - */ - val coverCache: CoverCache by injectLazy() - - private val downloadManager: DownloadManager by injectLazy() +class MangaInfoPresenter( + val manga: Manga, + val source: Source, + private val chapterCountRelay: BehaviorRelay, + private val mangaFavoriteRelay: PublishRelay, + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val coverCache: CoverCache = Injekt.get() +) : BasePresenter() { /** * Subscription to send the manga to the view. @@ -68,26 +46,16 @@ class MangaInfoPresenter : BasePresenter() { override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - - manga = SharedData.get(MangaEvent::class.java)?.manga ?: return - source = sourceManager.get(manga.source) ?: run { - context.toast("Could not find manga source!") - return - } sendMangaToView() // Update chapter count - SharedData.get(ChapterCountEvent::class.java)?.observable - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribeLatestCache(MangaInfoFragment::setChapterCount) + chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(MangaInfoController::setChapterCount) // Update favorite status - SharedData.get(MangaFavoriteEvent::class.java)?.let { - it.observable - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { setFavorite(it) } - .apply { add(this) } - } + mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribe { setFavorite(it) } + .apply { add(this) } } /** @@ -114,9 +82,9 @@ class MangaInfoPresenter : BasePresenter() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnNext { sendMangaToView() } - .subscribeFirst({ view, manga -> + .subscribeFirst({ view, _ -> view.onFetchMangaDone() - }, { view, error -> + }, { view, _ -> view.onFetchMangaError() }) } @@ -163,7 +131,7 @@ class MangaInfoPresenter : BasePresenter() { * @return List of categories, default plus user categories */ fun getCategories(): List { - return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking() + return db.getCategories().executeAsBlocking() } /** @@ -172,34 +140,30 @@ class MangaInfoPresenter : BasePresenter() { * @param manga the manga to get categories from. * @return Array of category ids the manga is in, if none returns default id */ - fun getMangaCategoryIds(manga: Manga): Array { + fun getMangaCategoryIds(manga: Manga): Array { val categories = db.getCategoriesForManga(manga).executeAsBlocking() - if(categories.isEmpty()) { - return arrayListOf(Category.createDefault().id).toTypedArray() - } - return categories.map { it.id }.toTypedArray() + return categories.mapNotNull { it.id }.toTypedArray() } /** * Move the given manga to categories. * - * @param categories the selected categories. * @param manga the manga to move. + * @param categories the selected categories. */ - fun moveMangaToCategories(categories: List, manga: Manga) { - val mc = categories.map { MangaCategory.create(manga, it) } - - db.setMangaCategories(mc, arrayListOf(manga)) + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) } /** * Move the given manga to the category. * - * @param category the selected category. * @param manga the manga to move. + * @param category the selected category, or null for default category. */ - fun moveMangaToCategory(category: Category, manga: Manga) { - moveMangaToCategories(arrayListOf(category), manga) + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) } } 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 new file mode 100644 index 000000000..6b1bf0a22 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackChaptersDialog : DialogController + where T : Controller, T : SetTrackChaptersDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.chapters) + .customView(R.layout.track_chapters_dialog, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog, _ -> + val view = dialog.customView + if (view != null) { + // Remove focus to update selected number + val np = view.findViewById(R.id.chapters_picker) as NumberPicker + np.clearFocus() + + (targetController as? Listener)?.setChaptersRead(item, np.value) + } + } + .build() + + val view = dialog.customView + if (view != null) { + val np = view.findViewById(R.id.chapters_picker) as NumberPicker + // Set initial value + np.value = item.track?.last_chapter_read ?: 0 + // Don't allow to go from 0 to 9999 + np.wrapSelectorWheel = false + } + + return dialog + } + + interface Listener { + fun setChaptersRead(item: TrackItem, chaptersRead: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt new file mode 100644 index 000000000..80931940a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt @@ -0,0 +1,80 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackScoreDialog : DialogController + where T : Controller, T : SetTrackScoreDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.score) + .customView(R.layout.track_score_dialog, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog, _ -> + val view = dialog.customView + if (view != null) { + // Remove focus to update selected number + val np = view.findViewById(R.id.score_picker) as NumberPicker + np.clearFocus() + + (targetController as? Listener)?.setScore(item, np.value) + } + } + .show() + + val view = dialog.customView + if (view != null) { + val np = view.findViewById(R.id.score_picker) as NumberPicker + val scores = item.service.getScoreList().toTypedArray() + np.maxValue = scores.size - 1 + np.displayedValues = scores + + // Set initial value + val displayedScore = item.service.displayScore(item.track!!) + if (displayedScore != "-") { + val index = scores.indexOf(displayedScore) + np.value = if (index != -1) index else 0 + } + } + + return dialog + } + + interface Listener { + fun setScore(item: TrackItem, score: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt new file mode 100644 index 000000000..6ad057951 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackStatusDialog : DialogController + where T : Controller, T : SetTrackStatusDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + val statusList = item.service.getStatusList().orEmpty() + val statusString = statusList.mapNotNull { item.service.getStatus(it) } + val selectedIndex = statusList.indexOf(item.track?.status) + + return MaterialDialog.Builder(activity!!) + .title(R.string.status) + .negativeText(android.R.string.cancel) + .items(statusString) + .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ -> + (targetController as? Listener)?.setStatus(item, i) + true + }) + .build() + } + + interface Listener { + fun setStatus(item: TrackItem, selection: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index 08e727b97..bc0b5ec5b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -1,33 +1,44 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.support.v7.widget.RecyclerView -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.inflate - -class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter() { - - var items = emptyList() - set(value) { - if (field !== value) { - field = value - notifyDataSetChanged() - } - } - - var onClickListener: (TrackItem) -> Unit = {} - - override fun getItemCount(): Int { - return items.size - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { - val view = parent.inflate(R.layout.item_track) - return TrackHolder(view, fragment) - } - - override fun onBindViewHolder(holder: TrackHolder, position: Int) { - holder.onSetValues(items[position]) - } - -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.manga.track + +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.inflate + +class TrackAdapter(controller: TrackController) : RecyclerView.Adapter() { + + var items = emptyList() + set(value) { + if (field !== value) { + field = value + notifyDataSetChanged() + } + } + + val rowClickListener: OnRowClickListener = controller + + fun getItem(index: Int): TrackItem? { + return items.getOrNull(index) + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { + val view = parent.inflate(R.layout.track_item) + return TrackHolder(view, this) + } + + override fun onBindViewHolder(holder: TrackHolder, position: Int) { + holder.bind(items[position]) + } + + interface OnRowClickListener { + fun onTitleClick(position: Int) + fun onStatusClick(position: Int) + fun onChaptersClick(position: Int) + fun onScoreClick(position: Int) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt new file mode 100644 index 000000000..363c0fe78 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt @@ -0,0 +1,129 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.track_controller.view.* + +class TrackController : NucleusController(), + TrackAdapter.OnRowClickListener, + SetTrackStatusDialog.Listener, + SetTrackChaptersDialog.Listener, + SetTrackScoreDialog.Listener { + + private var adapter: TrackAdapter? = null + + init { + // There's no menu, but this avoids a bug when coming from the catalogue, where the menu + // disappears if the searchview is expanded + setHasOptionsMenu(true) + } + + override fun createPresenter(): TrackPresenter { + return TrackPresenter((parentController as MangaController).manga!!) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.track_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = TrackAdapter(this) + with(view) { + track_recycler.layoutManager = LinearLayoutManager(context) + track_recycler.adapter = adapter + swipe_refresh.isEnabled = false + swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() } + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + fun onNextTrackings(trackings: List) { + val atLeastOneLink = trackings.any { it.track != null } + adapter?.items = trackings + view?.swipe_refresh?.isEnabled = atLeastOneLink + (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) + } + + fun onSearchResults(results: List) { + getSearchDialog()?.onSearchResults(results) + } + + @Suppress("UNUSED_PARAMETER") + fun onSearchResultsError(error: Throwable) { + getSearchDialog()?.onSearchResultsError() + } + + private fun getSearchDialog(): TrackSearchDialog? { + return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog + } + + fun onRefreshDone() { + view?.swipe_refresh?.isRefreshing = false + } + + fun onRefreshError(error: Throwable) { + view?.swipe_refresh?.isRefreshing = false + activity?.toast(error.message) + } + + override fun onTitleClick(position: Int) { + val item = adapter?.getItem(position) ?: return + TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER) + } + + override fun onStatusClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackStatusDialog(this, item).showDialog(router) + } + + override fun onChaptersClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackChaptersDialog(this, item).showDialog(router) + } + + override fun onScoreClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackScoreDialog(this, item).showDialog(router) + } + + override fun setStatus(item: TrackItem, selection: Int) { + presenter.setStatus(item, selection) + view?.swipe_refresh?.isRefreshing = true + } + + override fun setScore(item: TrackItem, score: Int) { + presenter.setScore(item, score) + view?.swipe_refresh?.isRefreshing = true + } + + override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { + presenter.setLastChapterRead(item, chaptersRead) + view?.swipe_refresh?.isRefreshing = true + } + + private companion object { + const val TAG_SEARCH_CONTROLLER = "track_search_controller" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt deleted file mode 100755 index 017ef8703..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt +++ /dev/null @@ -1,173 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import android.support.v7.widget.LinearLayoutManager -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.NumberPicker -import com.afollestad.materialdialogs.MaterialDialog -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.fragment_track.* -import nucleus.factory.RequiresPresenter - -@RequiresPresenter(TrackPresenter::class) -class TrackFragment : BaseRxFragment() { - - companion object { - fun newInstance(): TrackFragment { - return TrackFragment() - } - } - - private lateinit var adapter: TrackAdapter - - private var dialog: TrackSearchDialog? = null - - private val searchFragmentTag: String - get() = "search_fragment" - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View { - return inflater.inflate(R.layout.fragment_track, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - adapter = TrackAdapter(this) - recycler.layoutManager = LinearLayoutManager(context) - recycler.adapter = adapter - swipe_refresh.isEnabled = false - swipe_refresh.setOnRefreshListener { presenter.refresh() } - } - - private fun findSearchFragmentIfNeeded() { - if (dialog == null) { - dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as? TrackSearchDialog - } - } - - fun onNextTrackings(trackings: List) { - adapter.items = trackings - swipe_refresh.isEnabled = trackings.any { it.track != null } - (activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null }) - } - - fun onSearchResults(results: List) { - if (!isResumed) return - - findSearchFragmentIfNeeded() - dialog?.onSearchResults(results) - } - - fun onSearchResultsError(error: Throwable) { - if (!isResumed) return - - findSearchFragmentIfNeeded() - dialog?.onSearchResultsError() - } - - fun onRefreshDone() { - swipe_refresh.isRefreshing = false - } - - fun onRefreshError(error: Throwable) { - swipe_refresh.isRefreshing = false - context.toast(error.message) - } - - fun onTitleClick(item: TrackItem) { - if (!isResumed) return - - if (dialog == null) { - dialog = TrackSearchDialog.newInstance() - } - - presenter.selectedService = item.service - dialog?.show(childFragmentManager, searchFragmentTag) - } - - fun onStatusClick(item: TrackItem) { - if (!isResumed || item.track == null) return - - val statusList = item.service.getStatusList().map { item.service.getStatus(it) } - val selectedIndex = item.service.getStatusList().indexOf(item.track.status) - - MaterialDialog.Builder(context) - .title(R.string.status) - .items(statusList) - .itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence -> - presenter.setStatus(item, i) - swipe_refresh.isRefreshing = true - true - }) - .show() - } - - fun onChaptersClick(item: TrackItem) { - if (!isResumed || item.track == null) return - - val dialog = MaterialDialog.Builder(context) - .title(R.string.chapters) - .customView(R.layout.dialog_track_chapters, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { d, action -> - val view = d.customView - if (view != null) { - val np = view.findViewById(R.id.chapters_picker) as NumberPicker - np.clearFocus() - presenter.setLastChapterRead(item, np.value) - swipe_refresh.isRefreshing = true - } - } - .show() - - val view = dialog.customView - if (view != null) { - val np = view.findViewById(R.id.chapters_picker) as NumberPicker - // Set initial value - np.value = item.track.last_chapter_read - // Don't allow to go from 0 to 9999 - np.wrapSelectorWheel = false - } - } - - fun onScoreClick(item: TrackItem) { - if (!isResumed || item.track == null) return - - val dialog = MaterialDialog.Builder(activity) - .title(R.string.score) - .customView(R.layout.dialog_track_score, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { d, action -> - val view = d.customView - if (view != null) { - val np = view.findViewById(R.id.score_picker) as NumberPicker - np.clearFocus() - presenter.setScore(item, np.value) - swipe_refresh.isRefreshing = true - } - } - .show() - - val view = dialog.customView - if (view != null) { - val np = view.findViewById(R.id.score_picker) as NumberPicker - val scores = item.service.getScoreList().toTypedArray() - np.maxValue = scores.size - 1 - np.displayedValues = scores - - // Set initial value - val displayedScore = item.service.displayScore(item.track) - if (displayedScore != "-") { - val index = scores.indexOf(displayedScore) - np.value = if (index != -1) index else 0 - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index 9ca33f692..e5972f296 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -1,42 +1,41 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.support.v7.widget.RecyclerView -import android.view.View -import eu.kanade.tachiyomi.R -import kotlinx.android.synthetic.main.item_track.view.* - -class TrackHolder(private val view: View, private val fragment: TrackFragment) -: RecyclerView.ViewHolder(view) { - - private lateinit var item: TrackItem - - init { - view.title_container.setOnClickListener { fragment.onTitleClick(item) } - view.status_container.setOnClickListener { fragment.onStatusClick(item) } - view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) } - view.score_container.setOnClickListener { fragment.onScoreClick(item) } - } - - @Suppress("DEPRECATION") - fun onSetValues(item: TrackItem) = with(view) { - this@TrackHolder.item = item - val track = item.track - track_logo.setImageResource(item.service.getLogo()) - logo.setBackgroundColor(item.service.getLogoColor()) - if (track != null) { - track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary) - track_title.setAllCaps(false) - track_title.text = track.title - track_chapters.text = "${track.last_chapter_read}/" + - if (track.total_chapters > 0) track.total_chapters else "-" - track_status.text = item.service.getStatus(track.status) - track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) - } else { - track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) - track_title.setText(R.string.action_edit) - track_chapters.text = "" - track_score.text = "" - track_status.text = "" - } - } -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.manga.track + +import android.annotation.SuppressLint +import android.support.v7.widget.RecyclerView +import android.view.View +import eu.kanade.tachiyomi.R +import kotlinx.android.synthetic.main.track_item.view.* + +class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) { + + init { + val listener = adapter.rowClickListener + view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } + view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } + view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } + view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } + } + + @SuppressLint("SetTextI18n") + @Suppress("DEPRECATION") + fun bind(item: TrackItem) = with(itemView) { + val track = item.track + track_logo.setImageResource(item.service.getLogo()) + logo.setBackgroundColor(item.service.getLogoColor()) + if (track != null) { + track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary) + track_title.setAllCaps(false) + track_title.text = track.title + track_chapters.text = "${track.last_chapter_read}/" + + if (track.total_chapters > 0) track.total_chapters else "-" + track_status.text = item.service.getStatus(track.status) + track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) + } else { + track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) + track_title.setText(R.string.action_edit) + track_chapters.text = "" + track_score.text = "" + track_status.text = "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt index 9a435cd32..6e7c3ebec 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt @@ -1,8 +1,6 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService - -class TrackItem(val track: Track?, val service: TrackService) { - -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.manga.track + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService + +data class TrackItem(val track: Track?, val service: TrackService) 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 6d799b0c4..b9e152ba4 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 @@ -1,137 +1,129 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.MangaEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.toast -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy - -class TrackPresenter : BasePresenter() { - - private val db: DatabaseHelper by injectLazy() - - private val trackManager: TrackManager by injectLazy() - - lateinit var manga: Manga - private set - - private var trackList: List = emptyList() - - private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - - var selectedService: TrackService? = null - - private var trackSubscription: Subscription? = null - - private var searchSubscription: Subscription? = null - - private var refreshSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - manga = SharedData.get(MangaEvent::class.java)?.manga ?: return - fetchTrackings() - } - - fun fetchTrackings() { - trackSubscription?.let { remove(it) } - trackSubscription = db.getTracks(manga) - .asRxObservable() - .map { tracks -> - loggedServices.map { service -> - TrackItem(tracks.find { it.sync_id == service.id }, service) - } - } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { trackList = it } - .subscribeLatestCache(TrackFragment::onNextTrackings) - } - - fun refresh() { - refreshSubscription?.let { remove(it) } - refreshSubscription = Observable.from(trackList) - .filter { it.track != null } - .concatMap { item -> - item.service.refresh(item.track!!) - .flatMap { db.insertTrack(it).asRxObservable() } - .map { item } - .onErrorReturn { item } - } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> view.onRefreshDone() }, - TrackFragment::onRefreshError) - } - - fun search(query: String) { - val service = selectedService ?: return - - searchSubscription?.let { remove(it) } - searchSubscription = service.search(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(TrackFragment::onSearchResults, - TrackFragment::onSearchResultsError) - } - - fun registerTracking(item: Track?) { - val service = selectedService ?: return - - if (item != null) { - item.manga_id = manga.id!! - add(service.bind(item) - .flatMap { db.insertTrack(item).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ }, - { error -> context.toast(error.message) })) - } else { - db.deleteTrackForManga(manga, service).executeAsBlocking() - } - } - - private fun updateRemote(track: Track, service: TrackService) { - service.update(track) - .flatMap { db.insertTrack(track).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> view.onRefreshDone() }, - { view, error -> - view.onRefreshError(error) - - // Restart on error to set old values - fetchTrackings() - }) - } - - fun setStatus(item: TrackItem, index: Int) { - val track = item.track!! - track.status = item.service.getStatusList()[index] - updateRemote(track, item.service) - } - - fun setScore(item: TrackItem, index: Int) { - val track = item.track!! - track.score = item.service.indexToScore(index) - updateRemote(track, item.service) - } - - fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { - val track = item.track!! - track.last_chapter_read = chapterNumber - updateRemote(track, item.service) - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.os.Bundle +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.toast +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class TrackPresenter( + val manga: Manga, + preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val trackManager: TrackManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + private var trackList: List = emptyList() + + private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } + + private var trackSubscription: Subscription? = null + + private var searchSubscription: Subscription? = null + + private var refreshSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + fetchTrackings() + } + + fun fetchTrackings() { + trackSubscription?.let { remove(it) } + trackSubscription = db.getTracks(manga) + .asRxObservable() + .map { tracks -> + loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { trackList = it } + .subscribeLatestCache(TrackController::onNextTrackings) + } + + fun refresh() { + refreshSubscription?.let { remove(it) } + refreshSubscription = Observable.from(trackList) + .filter { it.track != null } + .concatMap { item -> + item.service.refresh(item.track!!) + .flatMap { db.insertTrack(it).asRxObservable() } + .map { item } + .onErrorReturn { item } + } + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, result -> view.onRefreshDone() }, + TrackController::onRefreshError) + } + + fun search(query: String, service: TrackService) { + searchSubscription?.let { remove(it) } + searchSubscription = service.search(query) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(TrackController::onSearchResults, + TrackController::onSearchResultsError) + } + + fun registerTracking(item: Track?, service: TrackService) { + if (item != null) { + item.manga_id = manga.id!! + add(service.bind(item) + .flatMap { db.insertTrack(item).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ }, + { error -> context.toast(error.message) })) + } else { + db.deleteTrackForManga(manga, service).executeAsBlocking() + } + } + + private fun updateRemote(track: Track, service: TrackService) { + service.update(track) + .flatMap { db.insertTrack(track).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, result -> view.onRefreshDone() }, + { view, error -> + view.onRefreshError(error) + + // Restart on error to set old values + fetchTrackings() + }) + } + + fun setStatus(item: TrackItem, index: Int) { + val track = item.track!! + track.status = item.service.getStatusList()[index] + updateRemote(track, item.service) + } + + fun setScore(item: TrackItem, index: Int) { + val track = item.track!! + track.score = item.service.indexToScore(index) + updateRemote(track, item.service) + } + + fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { + val track = item.track!! + track.last_chapter_read = chapterNumber + updateRemote(track, item.service) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt index 7aa5f7653..31b8bf89d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt @@ -1,47 +1,47 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.util.inflate -import kotlinx.android.synthetic.main.item_track_search.view.* -import java.util.* - -class TrackSearchAdapter(context: Context) -: ArrayAdapter(context, R.layout.item_track_search, ArrayList()) { - - override fun getView(position: Int, view: View?, parent: ViewGroup): View { - var v = view - // Get the data item for this position - val track = getItem(position) - // Check if an existing view is being reused, otherwise inflate the view - val holder: TrackSearchHolder // view lookup cache stored in tag - if (v == null) { - v = parent.inflate(R.layout.item_track_search) - holder = TrackSearchHolder(v) - v.tag = holder - } else { - holder = v.tag as TrackSearchHolder - } - holder.onSetValues(track) - return v - } - - fun setItems(syncs: List) { - setNotifyOnChange(false) - clear() - addAll(syncs) - notifyDataSetChanged() - } - - class TrackSearchHolder(private val view: View) { - - fun onSetValues(track: Track) { - view.track_search_title.text = track.title - } - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.track_search_item.view.* +import java.util.* + +class TrackSearchAdapter(context: Context) +: ArrayAdapter(context, R.layout.track_search_item, ArrayList()) { + + override fun getView(position: Int, view: View?, parent: ViewGroup): View { + var v = view + // Get the data item for this position + val track = getItem(position) + // Check if an existing view is being reused, otherwise inflate the view + val holder: TrackSearchHolder // view lookup cache stored in tag + if (v == null) { + v = parent.inflate(R.layout.track_search_item) + holder = TrackSearchHolder(v) + v.tag = holder + } else { + holder = v.tag as TrackSearchHolder + } + holder.onSetValues(track) + return v + } + + fun setItems(syncs: List) { + setNotifyOnChange(false) + clear() + addAll(syncs) + notifyDataSetChanged() + } + + class TrackSearchHolder(private val view: View) { + + fun onSetValues(track: Track) { + view.track_search_title.text = track.title + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index 787180600..c9ce8cd66 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -1,119 +1,144 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.app.Dialog -import android.os.Bundle -import android.support.v4.app.DialogFragment -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.widget.SimpleTextWatcher -import kotlinx.android.synthetic.main.dialog_track_search.view.* -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -class TrackSearchDialog : DialogFragment() { - - companion object { - - fun newInstance(): TrackSearchDialog { - return TrackSearchDialog() - } - } - - private lateinit var v: View - - lateinit var adapter: TrackSearchAdapter - private set - - private val queryRelay by lazy { PublishRelay.create() } - - private var searchDebounceSubscription: Subscription? = null - - private var selectedItem: Track? = null - - val presenter: TrackPresenter - get() = (parentFragment as TrackFragment).presenter - - override fun onCreateDialog(savedState: Bundle?): Dialog { - val dialog = MaterialDialog.Builder(context) - .customView(R.layout.dialog_track_search, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog1, which -> onPositiveButtonClick() } - .build() - - onViewCreated(dialog.view, savedState) - - return dialog - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - v = view - - // Create adapter - adapter = TrackSearchAdapter(context) - view.track_search_list.adapter = adapter - - // Set listeners - selectedItem = null - view.track_search_list.setOnItemClickListener { parent, viewList, position, id -> - selectedItem = adapter.getItem(position) - } - - // Do an initial search based on the manga's title - if (savedState == null) { - val title = presenter.manga.title - view.track_search.append(title) - search(title) - } - - view.track_search.addTextChangedListener(object : SimpleTextWatcher() { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - queryRelay.call(s.toString()) - } - }) - } - - override fun onResume() { - super.onResume() - - // Listen to text changes - searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .filter { it.isNotBlank() } - .subscribe { search(it) } - } - - override fun onPause() { - searchDebounceSubscription?.unsubscribe() - super.onPause() - } - - private fun search(query: String) { - v.progress.visibility = View.VISIBLE - v.track_search_list.visibility = View.GONE - - presenter.search(query) - } - - fun onSearchResults(results: List) { - selectedItem = null - v.progress.visibility = View.GONE - v.track_search_list.visibility = View.VISIBLE - adapter.setItems(results) - } - - fun onSearchResultsError() { - v.progress.visibility = View.VISIBLE - v.track_search_list.visibility = View.GONE - adapter.setItems(emptyList()) - } - - private fun onPositiveButtonClick() { - presenter.registerTracking(selectedItem) - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import com.jakewharton.rxbinding.widget.itemClicks +import com.jakewharton.rxbinding.widget.textChanges +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.plusAssign +import kotlinx.android.synthetic.main.track_search_dialog.view.* +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +class TrackSearchDialog : DialogController { + + private var dialogView: View? = null + + private var adapter: TrackSearchAdapter? = null + + private var selectedItem: Track? = null + + private val service: TrackService + + private var subscriptions = CompositeSubscription() + + private var searchTextSubscription: Subscription? = null + + private val trackController + get() = targetController as TrackController + + constructor(target: TrackController, service: TrackService) : super(Bundle().apply { + putInt(KEY_SERVICE, service.id) + }) { + targetController = target + this.service = service + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + service = Injekt.get().getService(bundle.getInt(KEY_SERVICE))!! + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity!!) + .customView(R.layout.track_search_dialog, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> onPositiveButtonClick() } + .build() + + if (subscriptions.isUnsubscribed) { + subscriptions = CompositeSubscription() + } + + dialogView = dialog.view + onViewCreated(dialog.view, savedState) + + return dialog + } + + fun onViewCreated(view: View, savedState: Bundle?) { + // Create adapter + val adapter = TrackSearchAdapter(view.context) + this.adapter = adapter + view.track_search_list.adapter = adapter + + // Set listeners + selectedItem = null + + subscriptions += view.track_search_list.itemClicks().subscribe { position -> + selectedItem = adapter.getItem(position) + } + + // Do an initial search based on the manga's title + if (savedState == null) { + val title = trackController.presenter.manga.title + view.track_search.append(title) + search(title) + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + subscriptions.unsubscribe() + dialogView = null + adapter = null + } + + override fun onAttach(view: View) { + super.onAttach(view) + searchTextSubscription = dialogView!!.track_search.textChanges() + .skip(1) + .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) + .map { it.toString() } + .filter(String::isNotBlank) + .subscribe { search(it) } + } + + override fun onDetach(view: View) { + super.onDetach(view) + searchTextSubscription?.unsubscribe() + } + + private fun search(query: String) { + val view = dialogView ?: return + view.progress.visibility = View.VISIBLE + view.track_search_list.visibility = View.GONE + + trackController.presenter.search(query, service) + } + + fun onSearchResults(results: List) { + selectedItem = null + val view = dialogView ?: return + view.progress.visibility = View.GONE + view.track_search_list.visibility = View.VISIBLE + adapter?.setItems(results) + } + + fun onSearchResultsError() { + val view = dialogView ?: return + view.progress.visibility = View.VISIBLE + view.track_search_list.visibility = View.GONE + adapter?.setItems(emptyList()) + } + + private fun onPositiveButtonClick() { + trackController.presenter.registerTracking(selectedItem, service) + } + + private companion object { + const val KEY_SERVICE = "service_id" + } + } \ 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 289ea0c14..d49bba416 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 @@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.SimpleAnimationListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener -import kotlinx.android.synthetic.main.activity_reader.* +import kotlinx.android.synthetic.main.reader_activity.* import me.zhanghai.android.systemuihelper.SystemUiHelper import me.zhanghai.android.systemuihelper.SystemUiHelper.* import nucleus.factory.RequiresPresenter @@ -84,6 +84,8 @@ class ReaderActivity : BaseRxActivity() { private val volumeKeysEnabled by lazy { preferences.readWithVolumeKeys().getOrDefault() } + private val volumeKeysInverted by lazy { preferences.readWithVolumeKeysInverted().getOrDefault() } + val preferences by injectLazy() private var systemUi: SystemUiHelper? = null @@ -92,14 +94,18 @@ class ReaderActivity : BaseRxActivity() { override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - setContentView(R.layout.activity_reader) + setContentView(R.layout.reader_activity) if (savedState == null && SharedData.get(ReaderEvent::class.java) == null) { finish() return } - setupToolbar(toolbar) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { + onBackPressed() + } initializeSettings() initializeBottomMenu() @@ -131,6 +137,7 @@ class ReaderActivity : BaseRxActivity() { } override fun onDestroy() { + toolbar.setNavigationOnClickListener(null) subscriptions.unsubscribe() viewer = null super.onDestroy() @@ -189,7 +196,7 @@ class ReaderActivity : BaseRxActivity() { KeyEvent.KEYCODE_VOLUME_DOWN -> { if (volumeKeysEnabled) { if (event.action == KeyEvent.ACTION_UP) { - viewer?.moveDown() + if (!volumeKeysInverted) viewer?.moveDown() else viewer?.moveUp() } return true } @@ -197,7 +204,7 @@ class ReaderActivity : BaseRxActivity() { KeyEvent.KEYCODE_VOLUME_UP -> { if (volumeKeysEnabled) { if (event.action == KeyEvent.ACTION_UP) { - viewer?.moveUp() + if (!volumeKeysInverted) viewer?.moveUp() else viewer?.moveDown() } return true } @@ -256,7 +263,7 @@ class ReaderActivity : BaseRxActivity() { // Invert the seekbar for the right to left reader page_seekbar.rotation = 180f } - setToolbarTitle(manga.title) + supportActionBar?.title = manga.title please_wait.visibility = View.VISIBLE please_wait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long)) } @@ -292,10 +299,10 @@ class ReaderActivity : BaseRxActivity() { page_seekbar.max = numPages - 1 page_seekbar.progress = currentPage - setToolbarSubtitle(if (chapter.isRecognizedNumber) + supportActionBar?.subtitle = if (chapter.isRecognizedNumber) getString(R.string.chapter_subtitle, decimalFormat.format(chapter.chapter_number.toDouble())) else - chapter.name) + chapter.name } fun onAppendChapter(chapter: ReaderChapter) { 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 4dc26c487..973869031 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 @@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.widget.SimpleSeekBarListener -import kotlinx.android.synthetic.main.dialog_reader_custom_filter.view.* +import kotlinx.android.synthetic.main.reader_custom_filter_dialog.view.* import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription @@ -65,7 +65,7 @@ class ReaderCustomFilterDialog : DialogFragment() { */ override fun onCreateDialog(savedState: Bundle?): Dialog { val dialog = MaterialDialog.Builder(activity) - .customView(R.layout.dialog_reader_custom_filter, false) + .customView(R.layout.reader_custom_filter_dialog, false) .positiveText(android.R.string.ok) .build() 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 38345e9f2..2a29a6cd9 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 @@ -29,7 +29,8 @@ import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import timber.log.Timber -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.io.File import java.net.URLConnection import java.util.* @@ -37,41 +38,17 @@ import java.util.* /** * Presenter of [ReaderActivity]. */ -class ReaderPresenter : BasePresenter() { - /** - * Preferences. - */ - val prefs: PreferencesHelper by injectLazy() +class ReaderPresenter( + val prefs: PreferencesHelper = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), + val downloadManager: DownloadManager = Injekt.get(), + val trackManager: TrackManager = Injekt.get(), + val sourceManager: SourceManager = Injekt.get(), + val chapterCache: ChapterCache = Injekt.get(), + val coverCache: CoverCache = Injekt.get() +) : BasePresenter() { - /** - * Database. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Download manager. - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Tracking manager. - */ - val trackManager: TrackManager by injectLazy() - - /** - * Source manager. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Chapter cache. - */ - val chapterCache: ChapterCache by injectLazy() - - /** - * Cover cache. - */ - val coverCache: CoverCache by injectLazy() + private val context = prefs.context /** * Manga being read. 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 bdcc558dd..32e6e6d34 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 @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener -import kotlinx.android.synthetic.main.dialog_reader_settings.view.* +import kotlinx.android.synthetic.main.reader_settings_dialog.view.* import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription @@ -26,7 +26,7 @@ class ReaderSettingsDialog : DialogFragment() { override fun onCreateDialog(savedState: Bundle?): Dialog { val dialog = MaterialDialog.Builder(activity) .title(R.string.label_settings) - .customView(R.layout.dialog_reader_settings, true) + .customView(R.layout.reader_settings_dialog, true) .positiveText(android.R.string.ok) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt index 878ab38df..ed1d12638 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base +import android.support.v4.app.Fragment import com.davemorrissey.labs.subscaleview.decoder.* import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderChapter import java.util.* @@ -12,7 +12,7 @@ import java.util.* * Base reader containing the common data that can be used by its implementations. It does not * contain any UI related action. */ -abstract class BaseReader : BaseFragment() { +abstract class BaseReader : Fragment() { companion object { /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt index b9314d0c4..559621932 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt @@ -6,7 +6,7 @@ import android.view.View import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import kotlinx.android.synthetic.main.page_decode_error.view.* +import kotlinx.android.synthetic.main.reader_page_decode_error.view.* class PageDecodeErrorLayout( val view: View, 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 a75eaaee7..6dcc2393e 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 @@ -16,8 +16,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader import eu.kanade.tachiyomi.util.inflate -import kotlinx.android.synthetic.main.chapter_image.view.* -import kotlinx.android.synthetic.main.item_pager_reader.view.* +import kotlinx.android.synthetic.main.reader_pager_item.view.* import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -259,7 +258,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? val activity = reader.activity as ReaderActivity - val layout = inflate(R.layout.page_decode_error) + val layout = inflate(R.layout.reader_page_decode_error) PageDecodeErrorLayout(layout, page, activity.readerTheme, { if (reader.isAdded) { activity.presenter.retryPage(page) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt index 0fa7a808f..c3c820f09 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt @@ -23,7 +23,7 @@ class PagerReaderAdapter(private val reader: PagerReader) : ViewPagerAdapter() { } override fun createView(container: ViewGroup, position: Int): View { - val view = container.inflate(R.layout.item_pager_reader) as PageView + val view = container.inflate(R.layout.reader_pager_item) as PageView view.initialize(reader, pages[position]) return view } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalViewPagerImpl.java b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalViewPagerImpl.java index 8f50151e2..d435dd24e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalViewPagerImpl.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalViewPagerImpl.java @@ -95,6 +95,7 @@ import java.util.List; * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java * complete} */ +@SuppressWarnings("deprecation") public class VerticalViewPagerImpl extends ViewGroup { private static final String TAG = "ViewPager"; private static final boolean DEBUG = false; 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 ea6c6dca9..373902027 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 @@ -51,7 +51,7 @@ class WebtoonAdapter(val fragment: WebtoonReader) : RecyclerView.Adapter(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : ConfirmDeleteChaptersDialog.Listener { + + private var chaptersToDelete = emptyList() + + constructor(target: T, chaptersToDelete: List) : this() { + this.chaptersToDelete = chaptersToDelete + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .content(R.string.confirm_delete_chapters) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + (targetController as? Listener)?.deleteChapters(chaptersToDelete) + } + .build() + } + + interface Listener { + fun deleteChapters(chaptersToDelete: List) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DateItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DateItem.kt index cc13088dd..de64e85b0 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 @@ -14,7 +14,7 @@ import java.util.* class DateItem(val date: Date) : AbstractHeaderItem() { override fun getLayoutRes(): Int { - return R.layout.item_recent_chapter_section + return R.layout.recent_chapters_section_item } override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt similarity index 51% rename from app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt index 4a1d86161..8bb9d57b9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DeletingChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt @@ -1,22 +1,27 @@ -package eu.kanade.tachiyomi.widget +package eu.kanade.tachiyomi.ui.recent_updates import android.app.Dialog import android.os.Bundle -import android.support.v4.app.DialogFragment import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Router import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController -class DeletingChaptersDialog : DialogFragment() { +class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) { companion object { const val TAG = "deleting_dialog" } override fun onCreateDialog(savedState: Bundle?): Dialog { - return MaterialDialog.Builder(activity) + return MaterialDialog.Builder(activity!!) .progress(true, 0) .content(R.string.deleting) .build() } + override fun showDialog(router: Router) { + showDialog(router, TAG) + } + } \ No newline at end of file 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 7ee7e1fe9..434ee400c 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,11 +2,14 @@ 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.util.getResourceColor -import kotlinx.android.synthetic.main.item_recent_chapters.view.* +import jp.wasabeef.glide.transformations.CropCircleTransformation +import kotlinx.android.synthetic.main.recent_chapters_item.view.* /** * Holder that contains chapter item @@ -41,6 +44,9 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // correctly positioned. The reason being that the view may change position before the // PopupMenu is shown. view.chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } } + view.manga_cover.setOnClickListener { + adapter.coverClickListener.onCoverClick(adapterPosition) + } } /** @@ -57,6 +63,16 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // Set manga title view.manga_title.text = item.manga.title + // Set cover + Glide.clear(itemView.manga_cover) + if (!item.manga.thumbnail_url.isNullOrEmpty()) { + Glide.with(itemView.context) + .load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .bitmapTransform(CropCircleTransformation(view.context)) + .into(itemView.manga_cover) + } + // Check if chapter is read and set correct color if (item.chapter.read) { view.chapter_title.setTextColor(readColor) @@ -115,7 +131,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> - with(adapter.fragment) { + with(adapter.controller) { when (menuItem.itemId) { R.id.action_download -> downloadChapter(item) R.id.action_delete -> deleteChapter(item) 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 7f1a1e4fd..a98287d78 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 @@ -24,14 +24,22 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem get() = status == Download.DOWNLOADED override fun getLayoutRes(): Int { - return R.layout.item_recent_chapters + return R.layout.recent_chapters_item } - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): RecentChapterHolder { - return RecentChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as RecentChaptersAdapter) + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): RecentChapterHolder { + + val view = inflater.inflate(layoutRes, parent, false) + return RecentChapterHolder(view , adapter as RecentChaptersAdapter) } - override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: RecentChapterHolder, position: Int, payloads: List?) { + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: RecentChapterHolder, + position: Int, + payloads: List?) { + holder.bind(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt index ebd1b7e69..233c90fce 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt @@ -3,11 +3,17 @@ package eu.kanade.tachiyomi.ui.recent_updates import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible -class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : - FlexibleAdapter>(null, fragment, true) { +class RecentChaptersAdapter(val controller: RecentChaptersController) : + FlexibleAdapter>(null, controller, true) { + + val coverClickListener: OnCoverClickListener = controller init { setDisplayHeadersAtStartUp(true) setStickyHeaders(true) } + + interface OnCoverClickListener { + fun onCoverClick(position: Int) + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt similarity index 55% rename from app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt index 338f28d51..e54a24beb 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt @@ -1,340 +1,340 @@ -package eu.kanade.tachiyomi.ui.recent_updates - -import android.os.Bundle -import android.support.v4.app.DialogFragment -import android.support.v7.app.AppCompatActivity -import android.support.v7.view.ActionMode -import android.support.v7.widget.DividerItemDecoration -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.view.* -import com.afollestad.materialdialogs.MaterialDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.DeletingChaptersDialog -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.fragment_recent_chapters.* -import nucleus.factory.RequiresPresenter -import timber.log.Timber - -/** - * Fragment that shows recent chapters. - * Uses [R.layout.fragment_recent_chapters]. - * UI related actions should be called from here. - */ -@RequiresPresenter(RecentChaptersPresenter::class) -class RecentChaptersFragment: - BaseRxFragment(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener{ - - companion object { - /** - * Create new RecentChaptersFragment. - * @return a new instance of [RecentChaptersFragment]. - */ - fun newInstance(): RecentChaptersFragment { - return RecentChaptersFragment() - } - } - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - /** - * Adapter containing the recent chapters. - */ - lateinit var adapter: RecentChaptersAdapter - private set - - /** - * Called when view gets created - * @param inflater layout inflater - * @param container view group - * @param savedState status of saved state - */ - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View { - // Inflate view - return inflater.inflate(R.layout.fragment_recent_chapters, container, false) - } - - /** - * Called when view is created - * @param view created view - * @param savedState status of saved sate - */ - override fun onViewCreated(view: View, savedState: Bundle?) { - // Init RecyclerView and adapter - recycler.layoutManager = LinearLayoutManager(activity) - recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) - adapter = RecentChaptersAdapter(this) - recycler.adapter = adapter - - recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) { - // Disable swipe refresh when view is not at the top - val firstPos = (recycler.layoutManager as LinearLayoutManager) - .findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos == 0 - } - }) - - swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) - swipe_refresh.setOnRefreshListener { - if (!LibraryUpdateService.isRunning(activity)) { - LibraryUpdateService.start(activity) - context.toast(R.string.action_update_library) - } - // It can be a very long operation, so we disable swipe refresh and show a toast. - swipe_refresh.isRefreshing = false - } - - // Update toolbar text - setToolbarTitle(R.string.label_recent_updates) - - // Disable toolbar elevation, it looks better with sticky headers. - activity.appbar.disableElevation() - } - - override fun onDestroyView() { - // Restore toolbar elevation. - activity.appbar.enableElevation() - super.onDestroyView() - } - - /** - * Returns selected chapters - * @return list of selected chapters - */ - fun getSelectedChapters(): List { - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } - } - - /** - * Called when item in list is clicked - * @param position position of clicked item - */ - override fun onItemClick(position: Int): Boolean { - // Get item from position - val item = adapter.getItem(position) as? RecentChapterItem ?: return false - if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item) - return false - } - } - - /** - * Called when item in list is long clicked - * @param position position of clicked item - */ - override fun onItemLongClick(position: Int) { - if (actionMode == null) - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - - toggleSelection(position) - } - - /** - * Called to toggle selection - * @param position position of selected item - */ - private fun toggleSelection(position: Int) { - adapter.toggleSelection(position) - - val count = adapter.selectedItemCount - if (count == 0) { - actionMode?.finish() - } else { - actionMode?.title = getString(R.string.label_selected, count) - } - } - - /** - * Open chapter in reader - * @param chapter selected chapter - */ - private fun openChapter(item: RecentChapterItem) { - val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) - startActivity(intent) - } - - /** - * Download selected items - * @param chapters list of selected [RecentChapter]s - */ - fun downloadChapters(chapters: List) { - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - } - - /** - * Populate adapter with chapters - * @param chapters list of [Any] - */ - fun onNextRecentChapters(chapters: List>) { - (activity as MainActivity).updateEmptyView(chapters.isEmpty(), - R.string.information_no_recent, R.drawable.ic_update_black_128dp) - - destroyActionModeIfNeeded() - adapter.updateDataSet(chapters.toMutableList()) - } - - /** - * Update download status of chapter - * @param download [Download] object containing download progress. - */ - fun onChapterStatusChange(download: Download) { - getHolder(download)?.notifyStatus(download.status) - } - - /** - * Returns holder belonging to chapter - * @param download [Download] object containing download progress. - */ - private fun getHolder(download: Download): RecentChapterHolder? { - return recycler.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder - } - - /** - * Mark chapter as read - * @param chapters list of chapters - */ - fun markAsRead(chapters: List) { - presenter.markChapterRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - /** - * Delete selected chapters - * @param chapters list of [RecentChapter] objects - */ - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) - presenter.deleteChapters(chapters) - } - - /** - * Destory [ActionMode] if it's shown - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - /** - * Mark chapter as unread - * @param chapters list of selected [RecentChapter] - */ - fun markAsUnread(chapters: List) { - presenter.markChapterRead(chapters, false) - } - - /** - * Start downloading chapter - * @param chapter selected chapter with manga - */ - fun downloadChapter(chapter: RecentChapterItem) { - presenter.downloadChapters(listOf(chapter)) - } - - /** - * Start deleting chapter - * @param chapter selected chapter with manga - */ - fun deleteChapter(chapter: RecentChapterItem) { - DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) - presenter.deleteChapters(listOf(chapter)) - } - - /** - * Called when chapters are deleted - */ - fun onChaptersDeleted() { - dismissDeletingDialog() - adapter.notifyDataSetChanged() - } - - /** - * Called when error while deleting - * @param error error message - */ - fun onChaptersDeletedError(error: Throwable) { - dismissDeletingDialog() - Timber.e(error) - } - - /** - * Called to dismiss deleting dialog - */ - fun dismissDeletingDialog() { - (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment) - ?.dismissAllowingStateLoss() - } - - /** - * Called when ActionMode item clicked - * @param mode the ActionMode object - * @param item item from ActionMode. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - if (!isAdded) return true - - when (item.itemId) { - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> { - MaterialDialog.Builder(activity) - .content(R.string.confirm_delete_chapters) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { dialog, action -> deleteChapters(getSelectedChapters()) } - .show() - } - else -> return false - } - return true - } - - /** - * Called when ActionMode created. - * @param mode the ActionMode object - * @param menu menu object of ActionMode - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) - adapter.mode = FlexibleAdapter.MODE_MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return false - } - - /** - * Called when ActionMode destroyed - * @param mode the ActionMode object - */ - override fun onDestroyActionMode(mode: ActionMode?) { - adapter.mode = FlexibleAdapter.MODE_IDLE - adapter.clearSelection() - actionMode = null - } - -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.recent_updates + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.view.ActionMode +import android.support.v7.widget.DividerItemDecoration +import android.support.v7.widget.LinearLayoutManager +import android.view.* +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.recent_chapters_controller.view.* +import timber.log.Timber + +/** + * Fragment that shows recent chapters. + * Uses [R.layout.recent_chapters_controller]. + * UI related actions should be called from here. + */ +class RecentChaptersController : NucleusController(), + NoToolbarElevationController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + FlexibleAdapter.OnUpdateListener, + ConfirmDeleteChaptersDialog.Listener, + RecentChaptersAdapter.OnCoverClickListener { + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing the recent chapters. + */ + var adapter: RecentChaptersAdapter? = null + private set + + override fun getTitle(): String? { + return resources?.getString(R.string.label_recent_updates) + } + + override fun createPresenter(): RecentChaptersPresenter { + return RecentChaptersPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.recent_chapters_controller, container, false) + } + + /** + * Called when view is created + * @param view created view + * @param savedViewState status of saved sate + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + // Init RecyclerView and adapter + val layoutManager = LinearLayoutManager(context) + recycler.layoutManager = layoutManager + recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + recycler.setHasFixedSize(true) + adapter = RecentChaptersAdapter(this@RecentChaptersController) + recycler.adapter = adapter + + recycler.scrollStateChanges().subscribeUntilDestroy { + // Disable swipe refresh when view is not at the top + val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition() + swipe_refresh.isEnabled = firstPos <= 0 + } + + swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) + swipe_refresh.refreshes().subscribeUntilDestroy { + if (!LibraryUpdateService.isRunning(context)) { + LibraryUpdateService.start(context) + context.toast(R.string.action_update_library) + } + // It can be a very long operation, so we disable swipe refresh and show a toast. + swipe_refresh.isRefreshing = false + } + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + actionMode = null + } + + /** + * Returns selected chapters + * @return list of selected chapters + */ + fun getSelectedChapters(): List { + val adapter = adapter ?: return emptyList() + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } + } + + /** + * Called when item in list is clicked + * @param position position of clicked item + */ + override fun onItemClick(position: Int): Boolean { + val adapter = adapter ?: return false + + // Get item from position + val item = adapter.getItem(position) as? RecentChapterItem ?: return false + if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + toggleSelection(position) + return true + } else { + openChapter(item) + return false + } + } + + /** + * Called when item in list is long clicked + * @param position position of clicked item + */ + override fun onItemLongClick(position: Int) { + if (actionMode == null) + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + + toggleSelection(position) + } + + /** + * Called to toggle selection + * @param position position of selected item + */ + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + adapter.toggleSelection(position) + actionMode?.invalidate() + } + + /** + * Open chapter in reader + * @param chapter selected chapter + */ + private fun openChapter(item: RecentChapterItem) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) + startActivity(intent) + } + + /** + * Download selected items + * @param chapters list of selected [RecentChapter]s + */ + fun downloadChapters(chapters: List) { + destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) + } + + /** + * Populate adapter with chapters + * @param chapters list of [Any] + */ + fun onNextRecentChapters(chapters: List>) { + destroyActionModeIfNeeded() + adapter?.updateDataSet(chapters.toMutableList()) + } + + override fun onUpdateEmptyView(size: Int) { + val emptyView = view?.empty_view ?: return + if (size > 0) { + emptyView.hide() + } else { + emptyView.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent) + } + } + + /** + * Update download status of chapter + * @param download [Download] object containing download progress. + */ + fun onChapterStatusChange(download: Download) { + getHolder(download)?.notifyStatus(download.status) + } + + /** + * Returns holder belonging to chapter + * @param download [Download] object containing download progress. + */ + private fun getHolder(download: Download): RecentChapterHolder? { + return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder + } + + /** + * Mark chapter as read + * @param chapters list of chapters + */ + fun markAsRead(chapters: List) { + presenter.markChapterRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } + + override fun deleteChapters(chaptersToDelete: List) { + destroyActionModeIfNeeded() + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(chaptersToDelete) + } + + /** + * Destory [ActionMode] if it's shown + */ + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + /** + * Mark chapter as unread + * @param chapters list of selected [RecentChapter] + */ + fun markAsUnread(chapters: List) { + presenter.markChapterRead(chapters, false) + } + + /** + * Start downloading chapter + * @param chapter selected chapter with manga + */ + fun downloadChapter(chapter: RecentChapterItem) { + presenter.downloadChapters(listOf(chapter)) + } + + /** + * Start deleting chapter + * @param chapter selected chapter with manga + */ + fun deleteChapter(chapter: RecentChapterItem) { + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(listOf(chapter)) + } + + override fun onCoverClick(position: Int) { + val chapterClicked = adapter?.getItem(position) as? RecentChapterItem ?: return + openManga(chapterClicked) + + } + + fun openManga(chapter: RecentChapterItem) { + router.pushController(RouterTransaction.with(MangaController(chapter.manga)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + + /** + * Called when chapters are deleted + */ + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter?.notifyDataSetChanged() + } + + /** + * Called when error while deleting + * @param error error message + */ + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error) + } + + /** + * Called to dismiss deleting dialog + */ + fun dismissDeletingDialog() { + router.popControllerWithTag(DeletingChaptersDialog.TAG) + } + + /** + * Called when ActionMode created. + * @param mode the ActionMode object + * @param menu menu object of ActionMode + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) + adapter?.mode = FlexibleAdapter.MODE_MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = adapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + } + return false + } + + /** + * Called when ActionMode item clicked + * @param mode the ActionMode object + * @param item item from ActionMode. + */ + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters()) + .showDialog(router) + else -> return false + } + return true + } + + /** + * Called when ActionMode destroyed + * @param mode the ActionMode object + */ + override fun onDestroyActionMode(mode: ActionMode?) { + adapter?.mode = FlexibleAdapter.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 8fb4a5b31..fe7cebfb5 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 @@ -14,29 +14,18 @@ import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import timber.log.Timber -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.* -class RecentChaptersPresenter : BasePresenter() { - /** - * Used to connect to database - */ - val db: DatabaseHelper by injectLazy() +class RecentChaptersPresenter( + val preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get() +) : BasePresenter() { - /** - * Used to get settings - */ - val preferences: PreferencesHelper by injectLazy() - - /** - * Used to get information from download manager - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Used to get source from source id - */ - val sourceManager: SourceManager by injectLazy() + private val context = preferences.context /** * List containing chapter and manga information @@ -48,11 +37,11 @@ class RecentChaptersPresenter : BasePresenter() { getRecentChaptersObservable() .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(RecentChaptersFragment::onNextRecentChapters) + .subscribeLatestCache(RecentChaptersController::onNextRecentChapters) getChapterStatusObservable() - .subscribeLatestCache(RecentChaptersFragment::onChapterStatusChange, - { view, error -> Timber.e(error) }) + .subscribeLatestCache(RecentChaptersController::onChapterStatusChange, + { _, error -> Timber.e(error) }) } /** @@ -207,9 +196,9 @@ class RecentChaptersPresenter : BasePresenter() { .toList() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> + .subscribeFirst({ view, _ -> view.onChaptersDeleted() - }, RecentChaptersFragment::onChaptersDeletedError) + }, RecentChaptersController::onChaptersDeletedError) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt index e95b457ef..766ce466d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt @@ -1,57 +1,48 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.view.ViewGroup -import eu.davidea.flexibleadapter4.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.util.inflate import uy.kohesive.injekt.injectLazy +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols /** * Adapter of RecentlyReadHolder. * Connection between Fragment and Holder * Holder updates should be called from here. * - * @param fragment a RecentlyReadFragment object + * @param controller a RecentlyReadController object * @constructor creates an instance of the adapter. */ -class RecentlyReadAdapter(val fragment: RecentlyReadFragment) -: FlexibleAdapter() { +class RecentlyReadAdapter(controller: RecentlyReadController) +: FlexibleAdapter(null, controller, true) { val sourceManager by injectLazy() - /** - * Called when ViewHolder is created - * @param parent parent View - * @param viewType int containing viewType - */ - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentlyReadHolder { - val view = parent.inflate(R.layout.item_recently_read) - return RecentlyReadHolder(view, this) - } + val resumeClickListener: OnResumeClickListener = controller + + val removeClickListener: OnRemoveClickListener = controller + + val coverClickListener: OnCoverClickListener = controller /** - * Called when ViewHolder is bind - * @param holder bind holder - * @param position position of holder + * DecimalFormat used to display correct chapter number */ - override fun onBindViewHolder(holder: RecentlyReadHolder, position: Int) { - val item = getItem(position) - holder.onSetValues(item) + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + + interface OnResumeClickListener { + fun onResumeClick(position: Int) } - /** - * Update items - * @param items items - */ - fun setItems(items: List) { - mItems = items - notifyDataSetChanged() + interface OnRemoveClickListener { + fun onRemoveClick(position: Int) } - override fun updateDataSet(param: String?) { - // Empty function + interface OnCoverClickListener { + fun onCoverClick(position: Int) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt new file mode 100644 index 000000000..6410f1e53 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt @@ -0,0 +1,134 @@ +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 +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.recently_read_controller.view.* + +/** + * Fragment that shows recently read manga. + * Uses R.layout.fragment_recently_read. + * UI related actions should be called from here. + */ +class RecentlyReadController : NucleusController(), + FlexibleAdapter.OnUpdateListener, + RecentlyReadAdapter.OnRemoveClickListener, + RecentlyReadAdapter.OnResumeClickListener, + RecentlyReadAdapter.OnCoverClickListener, + RemoveHistoryDialog.Listener { + + /** + * Adapter containing the recent manga. + */ + var adapter: RecentlyReadAdapter? = null + private set + + override fun getTitle(): String? { + return resources?.getString(R.string.label_recent_manga) + } + + override fun createPresenter(): RecentlyReadPresenter { + return RecentlyReadPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.recently_read_controller, container, false) + } + + /** + * Called when view is created + * + * @param view created view + * @param savedViewState saved state of the view + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + // Initialize adapter + recycler.layoutManager = LinearLayoutManager(context) + adapter = RecentlyReadAdapter(this@RecentlyReadController) + recycler.setHasFixedSize(true) + recycler.adapter = adapter + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + /** + * Populate adapter with chapters + * + * @param mangaHistory list of manga history + */ + fun onNextManga(mangaHistory: List) { + adapter?.updateDataSet(mangaHistory.toList()) + } + + override fun onUpdateEmptyView(size: Int) { + val emptyView = view?.empty_view ?: return + if (size > 0) { + emptyView.hide() + } else { + emptyView.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga) + } + } + + 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 nextChapter = presenter.getNextChapter(chapter, manga) + if (nextChapter != null) { + val intent = ReaderActivity.newIntent(activity, manga, nextChapter) + startActivity(intent) + } else { + activity.toast(R.string.no_next_chapter) + } + } + + override fun onRemoveClick(position: Int) { + val adapter = adapter ?: return + if (position == RecyclerView.NO_POSITION) return + + val (manga, _, history) = adapter.getItem(position).mch + + RemoveHistoryDialog(this, manga, history).showDialog(router) + } + + override fun onCoverClick(position: Int) { + val manga = adapter?.getItem(position)?.mch?.manga ?: return + router.pushController(RouterTransaction.with(MangaController(manga)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + + override fun removeHistory(manga: Manga, history: History, all: Boolean) { + if (all) { + // Reset last read of chapter to 0L + presenter.removeAllFromHistory(manga.id!!) + } else { + // Remove all chapters belonging to manga from library + presenter.removeFromHistory(history) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt deleted file mode 100755 index 3930570a4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt +++ /dev/null @@ -1,139 +0,0 @@ -package eu.kanade.tachiyomi.ui.recently_read - -import android.os.Bundle -import android.support.v7.widget.LinearLayoutManager -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.fragment_recently_read.* -import nucleus.factory.RequiresPresenter - -/** - * Fragment that shows recently read manga. - * Uses R.layout.fragment_recently_read. - * UI related actions should be called from here. - */ -@RequiresPresenter(RecentlyReadPresenter::class) -class RecentlyReadFragment : BaseRxFragment() { - companion object { - /** - * Create new RecentChaptersFragment. - */ - fun newInstance(): RecentlyReadFragment { - return RecentlyReadFragment() - } - } - - /** - * Adapter containing the recent manga. - */ - lateinit var adapter: RecentlyReadAdapter - private set - - /** - * Called when view gets created - * - * @param inflater layout inflater - * @param container view group - * @param savedState status of saved state - */ - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_recently_read, container, false) - } - - /** - * Called when view is created - * - * @param view created view - * @param savedState status of saved sate - */ - override fun onViewCreated(view: View?, savedState: Bundle?) { - // Initialize adapter - recycler.layoutManager = LinearLayoutManager(activity) - adapter = RecentlyReadAdapter(this) - recycler.setHasFixedSize(true) - recycler.adapter = adapter - - // Update toolbar text - setToolbarTitle(R.string.label_recent_manga) - } - - /** - * Populate adapter with chapters - * - * @param mangaHistory list of manga history - */ - fun onNextManga(mangaHistory: List) { - (activity as MainActivity).updateEmptyView(mangaHistory.isEmpty(), - R.string.information_no_recent_manga, R.drawable.ic_glasses_black_128dp) - - adapter.setItems(mangaHistory) - } - - /** - * Reset last read of chapter to 0L - * @param history history belonging to chapter - */ - fun removeFromHistory(history: History) { - presenter.removeFromHistory(history) - } - - /** - * Removes all chapters belonging to manga from library - * @param mangaId id of manga - */ - fun removeAllFromHistory(mangaId: Long) { - presenter.removeAllFromHistory(mangaId) - } - - /** - * Open chapter to continue reading - * @param chapter chapter that is opened - * @param manga manga belonging to chapter - */ - fun openChapter(chapter: Chapter, manga: Manga) { - if (!chapter.read) { - val intent = ReaderActivity.newIntent(activity, manga, chapter) - startActivity(intent) - } else { - presenter.openNextChapter(chapter, manga) - } - } - - /** - * Called from the presenter when wanting to open the next chapter of the current one. - * @param chapter the next chapter or null if it doesn't exist. - * @param manga the manga of the chapter. - */ - fun onOpenNextChapter(chapter: Chapter?, manga: Manga) { - if (chapter == null) { - context.toast(R.string.no_next_chapter) - } - // Avoid crashes if the fragment isn't resumed, the event will be ignored but it's unlikely - // to happen. - else if (isResumed) { - val intent = ReaderActivity.newIntent(activity, manga, chapter) - startActivity(intent) - } - } - - /** - * Open manga info page - * @param manga manga belonging to info page - */ - fun openMangaInfo(manga: Manga) { - val intent = MangaActivity.newIntent(activity, manga, true) - startActivity(intent) - } - -} \ No newline at end of file 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 e63723fd0..2d4464ffa 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,17 +1,12 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.support.v7.widget.RecyclerView import android.view.View -import com.afollestad.materialdialogs.MaterialDialog 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.widget.DialogCheckboxView -import kotlinx.android.synthetic.main.item_recently_read.view.* -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols +import kotlinx.android.synthetic.main.recently_read_item.view.* import java.util.* /** @@ -23,39 +18,47 @@ import java.util.* * @param adapter the adapter handling this holder. * @constructor creates a new recent chapter holder. */ -class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter) - : RecyclerView.ViewHolder(view) { +class RecentlyReadHolder( + view: View, + val adapter: RecentlyReadAdapter +) : FlexibleViewHolder(view, adapter) { - /** - * DecimalFormat used to display correct chapter number - */ - private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) + init { + itemView.remove.setOnClickListener { + adapter.removeClickListener.onRemoveClick(adapterPosition) + } - private val df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + itemView.resume.setOnClickListener { + adapter.resumeClickListener.onResumeClick(adapterPosition) + } + + itemView.cover.setOnClickListener { + adapter.coverClickListener.onCoverClick(adapterPosition) + } + } /** * Set values of view * * @param item item containing history information */ - fun onSetValues(item: MangaChapterHistory) { + fun bind(item: MangaChapterHistory) { // Retrieve objects - val manga = item.manga - val chapter = item.chapter - val history = item.history + val (manga, chapter, history) = item // Set manga title itemView.manga_title.text = manga.title // Set source + chapter title - val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble()) + val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source) .format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber) // Set last read timestamp title - itemView.last_read.text = df.format(Date(history.last_read)) + itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read)) // Set cover + Glide.clear(itemView.cover) if (!manga.thumbnail_url.isNullOrEmpty()) { Glide.with(itemView.context) .load(manga) @@ -64,40 +67,6 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter) .into(itemView.cover) } - // Set remove clickListener - itemView.remove.setOnClickListener { - // Create custom view - val dialogCheckboxView = DialogCheckboxView(itemView.context).apply { - setDescription(R.string.dialog_with_checkbox_remove_description) - setOptionDescription(R.string.dialog_with_checkbox_reset) - } - MaterialDialog.Builder(itemView.context) - .title(R.string.action_remove) - .customView(dialogCheckboxView, true) - .positiveText(R.string.action_remove) - .negativeText(android.R.string.cancel) - .onPositive { materialDialog, dialogAction -> - // Check if user wants all chapters reset - if (dialogCheckboxView.isChecked()) { - adapter.fragment.removeAllFromHistory(manga.id!!) - } else { - adapter.fragment.removeFromHistory(history) - } - } - .onNegative { materialDialog, dialogAction -> - materialDialog.dismiss() - }.show() - } - - // Set continue reading clickListener - itemView.resume.setOnClickListener { - adapter.fragment.openChapter(chapter, manga) - } - - // Set open manga info clickListener - itemView.cover.setOnClickListener { - adapter.fragment.openMangaInfo(manga) - } } } 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 new file mode 100644 index 000000000..7ec7b0db4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.recently_read + +import android.view.LayoutInflater +import android.view.ViewGroup +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() { + + override fun getLayoutRes(): Int { + return R.layout.recently_read_item + } + + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): RecentlyReadHolder { + + val view = parent.inflate(layoutRes) + return RecentlyReadHolder(view, adapter as RecentlyReadAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: RecentlyReadHolder, + position: Int, + payloads: List?) { + + holder.bind(mch) + } + + override fun equals(other: Any?): Boolean { + if (other is RecentlyReadItem) { + return mch.manga.id == other.mch.manga.id + } + return false + } + + override fun hashCode(): Int { + return mch.manga.id!!.hashCode() + } +} \ No newline at end of file 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 9f50473a4..28abe7881 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,11 +5,9 @@ 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.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import rx.Observable import rx.android.schedulers.AndroidSchedulers -import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.util.* @@ -18,7 +16,7 @@ import java.util.* * Contains information and data for fragment. * Observable updates should be called from here. */ -class RecentlyReadPresenter : BasePresenter() { +class RecentlyReadPresenter : BasePresenter() { /** * Used to connect to database @@ -30,22 +28,21 @@ class RecentlyReadPresenter : BasePresenter() { // Used to get a list of recently read manga getRecentMangaObservable() - .subscribeLatestCache({ view, historyList -> - view.onNextManga(historyList) - }) + .subscribeLatestCache(RecentlyReadController::onNextManga) } /** * Get recent manga observable * @return list of history */ - fun getRecentMangaObservable(): Observable> { + fun getRecentMangaObservable(): Observable> { // Set date for recent manga val cal = Calendar.getInstance() cal.time = Date() cal.add(Calendar.MONTH, -1) return db.getRecentManga(cal.time).asRxObservable() + .map { it.map(::RecentlyReadItem) } .observeOn(AndroidSchedulers.mainThread()) } @@ -73,50 +70,39 @@ class RecentlyReadPresenter : BasePresenter() { } /** - * Open the next chapter instead of the current one. + * Retrieves the next chapter of the given one. + * * @param chapter the chapter of the history object. * @param manga the manga of the chapter. */ - fun openNextChapter(chapter: Chapter, manga: Manga) { + fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? { + if (!chapter.read) { + return chapter + } + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } else -> throw NotImplementedError("Unknown sorting method") } - db.getChapters(manga).asRxSingle() - .map { it.sortedWith(Comparator { c1, c2 -> sortFunction(c1, c2) }) } - .map { chapters -> - val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } - when (manga.sorting) { - Manga.SORTING_SOURCE -> { - chapters.getOrNull(currChapterIndex + 1) - } - Manga.SORTING_NUMBER -> { - val chapterNumber = chapter.chapter_number + val chapters = db.getChapters(manga).executeAsBlocking() + .sortedWith(Comparator { c1, c2 -> sortFunction(c1, c2) }) - var nextChapter: Chapter? = null - for (i in (currChapterIndex + 1) until chapters.size) { - val c = chapters[i] - if (c.chapter_number > chapterNumber && - c.chapter_number <= chapterNumber + 1) { + val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } + return when (manga.sorting) { + Manga.SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) + Manga.SORTING_NUMBER -> { + val chapterNumber = chapter.chapter_number - nextChapter = c - break - } - } - nextChapter + ((currChapterIndex + 1) until chapters.size) + .map { chapters[it] } + .firstOrNull { it.chapter_number > chapterNumber && + it.chapter_number <= chapterNumber + 1 } - else -> throw NotImplementedError("Unknown sorting method") - } - } - .toObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, chapter -> - view.onOpenNextChapter(chapter, manga) - }, { view, error -> - Timber.e(error) - }) + } + else -> throw NotImplementedError("Unknown sorting method") + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt new file mode 100644 index 000000000..8385b4ed4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.ui.recently_read + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.widget.DialogCheckboxView + +class RemoveHistoryDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T: RemoveHistoryDialog.Listener { + + private var manga: Manga? = null + + private var history: History? = null + + constructor(target: T, manga: Manga, history: History) : this() { + this.manga = manga + this.history = history + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + + // Create custom view + val dialogCheckboxView = DialogCheckboxView(activity).apply { + setDescription(R.string.dialog_with_checkbox_remove_description) + setOptionDescription(R.string.dialog_with_checkbox_reset) + } + + return MaterialDialog.Builder(activity) + .title(R.string.action_remove) + .customView(dialogCheckboxView, true) + .positiveText(R.string.action_remove) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> onPositive(dialogCheckboxView.isChecked()) } + .build() + } + + private fun onPositive(checked: Boolean) { + val target = targetController as? Listener ?: return + val manga = manga ?: return + val history = history ?: return + + target.removeHistory(manga, history, checked) + } + + interface Listener { + fun removeHistory(manga: Manga, history: History, all: Boolean) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt index b49282531..af0b6ffc7 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 @@ -8,6 +8,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout import android.widget.ProgressBar import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.main.MainActivity import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy @@ -41,7 +42,7 @@ class AnilistLoginActivity : AppCompatActivity() { private fun returnToSettings() { finish() - val intent = Intent(this, SettingsActivity::class.java) + val intent = Intent(this, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt new file mode 100644 index 000000000..df1e41cba --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -0,0 +1,103 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.content.Context +import android.support.graphics.drawable.VectorDrawableCompat +import android.support.v4.graphics.drawable.DrawableCompat +import android.support.v7.preference.* +import eu.kanade.tachiyomi.widget.preference.IntListPreference + +@DslMarker +@Target(AnnotationTarget.TYPE) +annotation class DSL + +inline fun PreferenceManager.newScreen(context: Context, block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { + return createPreferenceScreen(context).also { it.block() } +} + +inline fun PreferenceGroup.preference(block: (@DSL Preference).() -> Unit): Preference { + return initThenAdd(Preference(context), block) +} + +inline fun PreferenceGroup.switchPreference(block: (@DSL SwitchPreferenceCompat).() -> Unit): SwitchPreferenceCompat { + return initThenAdd(SwitchPreferenceCompat(context), block) +} + +inline fun PreferenceGroup.checkBoxPreference(block: (@DSL CheckBoxPreference).() -> Unit): CheckBoxPreference { + return initThenAdd(CheckBoxPreference(context), block) +} + +inline fun PreferenceGroup.editTextPreference(block: (@DSL EditTextPreference).() -> Unit): EditTextPreference { + return initThenAdd(EditTextPreference(context), block).also(::initDialog) +} + +inline fun PreferenceGroup.listPreference(block: (@DSL ListPreference).() -> Unit): ListPreference { + return initThenAdd(ListPreference(context), block).also(::initDialog) +} + +inline fun PreferenceGroup.intListPreference(block: (@DSL IntListPreference).() -> Unit): IntListPreference { + return initThenAdd(IntListPreference(context), block).also(::initDialog) +} + +inline fun PreferenceGroup.multiSelectListPreference(block: (@DSL MultiSelectListPreference).() -> Unit): MultiSelectListPreference { + return initThenAdd(MultiSelectListPreference(context), block).also(::initDialog) +} + +inline fun PreferenceScreen.preferenceCategory(block: (@DSL PreferenceCategory).() -> Unit): PreferenceCategory { + return addThenInit(PreferenceCategory(context), block) +} + +inline fun PreferenceScreen.preferenceScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { + return addThenInit(preferenceManager.createPreferenceScreen(context), block) +} + +fun initDialog(dialogPreference: DialogPreference) { + with(dialogPreference) { + if (dialogTitle == null) { + dialogTitle = title + } + } +} + +inline fun

PreferenceGroup.initThenAdd(p: P, block: P.() -> Unit): P { + return p.apply { block(); addPreference(this); } +} + +inline fun

PreferenceGroup.addThenInit(p: P, block: P.() -> Unit): P { + return p.apply { addPreference(this); block() } +} + +inline fun Preference.onClick(crossinline block: () -> Unit) { + setOnPreferenceClickListener { block(); true } +} + +inline fun Preference.onChange(crossinline block: (Any?) -> Boolean) { + setOnPreferenceChangeListener { _, newValue -> block(newValue) } +} + +var Preference.defaultValue: Any? + get() = null // set only + set(value) { setDefaultValue(value) } + +var Preference.titleRes: Int + get() = 0 // set only + set(value) { setTitle(value) } + +var Preference.iconRes: Int + get() = 0 // set only + set(value) { icon = VectorDrawableCompat.create(context.resources, value, context.theme) } + +var Preference.summaryRes: Int + get() = 0 // set only + set(value) { setSummary(value) } + +var Preference.iconTint: Int + get() = 0 // set only + set(value) { DrawableCompat.setTint(icon, value) } + +var ListPreference.entriesRes: Array + get() = emptyArray() // set only + set(value) { entries = value.map { context.getString(it) }.toTypedArray() } + +var MultiSelectListPreference.entriesRes: Array + get() = emptyArray() // set only + set(value) { entries = value.map { context.getString(it) }.toTypedArray() } 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 new file mode 100644 index 000000000..3aa9398e9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt @@ -0,0 +1,166 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.app.Dialog +import android.os.Bundle +import android.support.v7.preference.PreferenceScreen +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker +import eu.kanade.tachiyomi.data.updater.GithubUpdateResult +import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob +import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.toast +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import timber.log.Timber +import java.text.DateFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsAboutController : SettingsController() { + + /** + * Checks for new releases + */ + private val updateChecker by lazy { GithubUpdateChecker() } + + /** + * The subscribtion service of the obtained release object + */ + private var releaseSubscription: Subscription? = null + + private val isUpdaterEnabled = !BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_about + + switchPreference { + key = "acra.enable" + titleRes = R.string.pref_enable_acra + summaryRes = R.string.pref_acra_summary + defaultValue = true + } + switchPreference { + key = Keys.automaticUpdates + titleRes = R.string.pref_enable_automatic_updates + summaryRes = R.string.pref_enable_automatic_updates_summary + defaultValue = false + + if (isUpdaterEnabled) { + onChange { newValue -> + val checked = newValue as Boolean + if (checked) { + UpdateCheckerJob.setupTask() + } else { + UpdateCheckerJob.cancelTask() + } + true + } + } else { + isVisible = false + } + } + preference { + titleRes = R.string.version + summary = if (BuildConfig.DEBUG) + "r" + BuildConfig.COMMIT_COUNT + else + BuildConfig.VERSION_NAME + + if (isUpdaterEnabled) { + onClick { checkVersion() } + } + } + preference { + titleRes = R.string.build_time + summary = getFormattedBuildTime() + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + releaseSubscription?.unsubscribe() + releaseSubscription = null + } + + /** + * Checks version and shows a user prompt if an update is available. + */ + private fun checkVersion() { + if (activity == null) return + + activity?.toast(R.string.update_check_look_for_updates) + releaseSubscription?.unsubscribe() + releaseSubscription = updateChecker.checkForUpdate() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + when (result) { + is GithubUpdateResult.NewUpdate -> { + val body = result.release.changeLog + val url = result.release.downloadLink + + // Create confirmation window + NewUpdateDialogController(body, url).showDialog(router) + } + is GithubUpdateResult.NoNewUpdate -> { + activity?.toast(R.string.update_check_no_new_updates) + } + } + }, { error -> + Timber.e(error) + }) + } + + class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) { + + constructor(body: String, url: String) : this(Bundle().apply { + putString(BODY_KEY, body) + putString(URL_KEY, url) + }) + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.update_check_title) + .content(args.getString(BODY_KEY)) + .positiveText(R.string.update_check_confirm) + .negativeText(R.string.update_check_ignore) + .onPositive { _, _ -> + val appContext = applicationContext + if (appContext != null) { + // Start download + val url = args.getString(URL_KEY) + UpdateDownloaderService.downloadUpdate(appContext, url) + } + } + .build() + } + + private companion object { + const val BODY_KEY = "NewUpdateDialogController.body" + const val URL_KEY = "NewUpdateDialogController.key" + } + } + + private fun getFormattedBuildTime(): String { + try { + val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US) + inputDf.timeZone = TimeZone.getTimeZone("UTC") + val date = inputDf.parse(BuildConfig.BUILD_TIME) + + val outputDf = DateFormat.getDateTimeInstance( + DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) + outputDf.timeZone = TimeZone.getDefault() + + return outputDf.format(date) + } catch (e: ParseException) { + return BuildConfig.BUILD_TIME + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt deleted file mode 100755 index 1f445cd5d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt +++ /dev/null @@ -1,139 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.os.Bundle -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker -import eu.kanade.tachiyomi.data.updater.GithubUpdateResult -import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob -import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService -import eu.kanade.tachiyomi.util.toast -import net.xpece.android.support.preference.SwitchPreference -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import java.text.DateFormat -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.* - -class SettingsAboutFragment : SettingsFragment() { - - companion object { - fun newInstance(rootKey: String): SettingsAboutFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsAboutFragment().apply { arguments = args } - } - } - - /** - * Checks for new releases - */ - private val updateChecker by lazy { GithubUpdateChecker() } - - /** - * The subscribtion service of the obtained release object - */ - private var releaseSubscription: Subscription? = null - - val automaticUpdates: SwitchPreference by bindPref(R.string.pref_enable_automatic_updates_key) - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - val version = findPreference(getString(R.string.pref_version)) - val buildTime = findPreference(getString(R.string.pref_build_time)) - - version.summary = if (BuildConfig.DEBUG) - "r" + BuildConfig.COMMIT_COUNT - else - BuildConfig.VERSION_NAME - - if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) { - //Set onClickListener to check for new version - version.setOnPreferenceClickListener { - checkVersion() - true - } - - automaticUpdates.setOnPreferenceChangeListener { preference, any -> - val checked = any as Boolean - if (checked) { - UpdateCheckerJob.setupTask() - } else { - UpdateCheckerJob.cancelTask() - } - true - } - } else { - automaticUpdates.isVisible = false - } - - buildTime.summary = getFormattedBuildTime() - } - - override fun onDestroyView() { - releaseSubscription?.unsubscribe() - super.onDestroyView() - } - - private fun getFormattedBuildTime(): String { - try { - val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") - inputDf.timeZone = TimeZone.getTimeZone("UTC") - val date = inputDf.parse(BuildConfig.BUILD_TIME) - - val outputDf = DateFormat.getDateTimeInstance( - DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) - outputDf.timeZone = TimeZone.getDefault() - - return outputDf.format(date) - } catch (e: ParseException) { - return BuildConfig.BUILD_TIME - } - } - - /** - * Checks version and shows a user prompt if an update is available. - */ - private fun checkVersion() { - releaseSubscription?.unsubscribe() - - context.toast(R.string.update_check_look_for_updates) - - releaseSubscription = updateChecker.checkForUpdate() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ result -> - when (result) { - is GithubUpdateResult.NewUpdate -> { - val body = result.release.changeLog - val url = result.release.downloadLink - - // Create confirmation window - MaterialDialog.Builder(context) - .title(R.string.update_check_title) - .content(body) - .positiveText(getString(R.string.update_check_confirm)) - .negativeText(getString(R.string.update_check_ignore)) - .onPositive { dialog, which -> - // Start download - UpdateDownloaderService.downloadUpdate(context, url) - } - .show() - } - is GithubUpdateResult.NoNewUpdate -> { - context.toast(R.string.update_check_no_new_updates) - } - } - }, { error -> - Timber.e(error) - }) - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt deleted file mode 100755 index 2abc7de8c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt +++ /dev/null @@ -1,88 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.os.Bundle -import android.support.v7.preference.PreferenceFragmentCompat -import android.support.v7.preference.PreferenceScreen -import android.view.MenuItem -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import kotlinx.android.synthetic.main.toolbar.* -import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy -import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy.ReplaceFragment - -class SettingsActivity : BaseActivity(), - PreferenceFragmentCompat.OnPreferenceStartScreenCallback, - PreferenceScreenNavigationStrategy.ReplaceFragment.Callbacks { - - private lateinit var replaceFragmentStrategy: ReplaceFragment - - /** - * Flags to send to the parent activity for reacting to preference changes. - */ - var parentFlags = 0 - set(value) { - field = field or value - setResult(field) - } - - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) - setTitle(R.string.label_settings) - setContentView(R.layout.activity_preferences) - - replaceFragmentStrategy = ReplaceFragment(this, - R.anim.abc_fade_in, R.anim.abc_fade_out, - R.anim.abc_fade_in, R.anim.abc_fade_out) - - if (savedState == null) { - supportFragmentManager.beginTransaction() - .add(R.id.settings_content, SettingsFragment.newInstance(null), "Settings") - .commit() - } else { - parentFlags = savedState.getInt(SettingsActivity::parentFlags.name) - } - - setupToolbar(toolbar, backNavigation = false) - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(SettingsActivity::parentFlags.name, parentFlags) - super.onSaveInstanceState(outState) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressed() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onBuildPreferenceFragment(key: String?): PreferenceFragmentCompat { - return when (key) { - "general_screen" -> SettingsGeneralFragment.newInstance(key) - "downloads_screen" -> SettingsDownloadsFragment.newInstance(key) - "sources_screen" -> SettingsSourcesFragment.newInstance(key) - "tracking_screen" -> SettingsTrackingFragment.newInstance(key) - "backup_screen" -> SettingsBackupFragment.newInstance(key) - "advanced_screen" -> SettingsAdvancedFragment.newInstance(key) - "about_screen" -> SettingsAboutFragment.newInstance(key) - "eh_screen" -> SettingsEhFragment.newInstance(key) //EH - else -> SettingsFragment.newInstance(key) - } - } - - override fun onPreferenceStartScreen(p0: PreferenceFragmentCompat, p1: PreferenceScreen): Boolean { - replaceFragmentStrategy.onPreferenceStartScreen(supportFragmentManager, p0, p1) - return true - } - - companion object { - const val FLAG_THEME_CHANGED = 0x1 - const val FLAG_DATABASE_CLEARED = 0x2 - const val FLAG_LANG_CHANGED = 0x4 - const val FLAG_EH_RECREATE = 0x8 - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt new file mode 100644 index 000000000..07f69edde --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -0,0 +1,166 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.app.Dialog +import android.os.Bundle +import android.support.v7.preference.PreferenceScreen +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.util.toast +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.injectLazy + +class SettingsAdvancedController : SettingsController() { + + private val network: NetworkHelper by injectLazy() + + private val chapterCache: ChapterCache by injectLazy() + + private val db: DatabaseHelper by injectLazy() + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_advanced + + preference { + key = CLEAR_CACHE_KEY + titleRes = R.string.pref_clear_chapter_cache + summary = context.getString(R.string.used_cache, chapterCache.readableSize) + + onClick { clearChapterCache() } + } + preference { + titleRes = R.string.pref_clear_cookies + + onClick { + network.cookies.removeAll() + activity?.toast(R.string.cookies_cleared) + } + } + preference { + titleRes = R.string.pref_clear_database + summaryRes = R.string.pref_clear_database_summary + + onClick { + val ctrl = ClearDatabaseDialogController() + ctrl.targetController = this@SettingsAdvancedController + ctrl.showDialog(router) + } + } + preference { + titleRes = R.string.pref_refresh_library_metadata + summaryRes = R.string.pref_refresh_library_metadata_summary + + onClick { LibraryUpdateService.start(context, target = Target.DETAILS) } + } + preference { + titleRes = R.string.pref_refresh_library_tracking + summaryRes = R.string.pref_refresh_library_tracking_summary + + onClick { LibraryUpdateService.start(context, target = Target.TRACKING) } + } + } + + private fun clearChapterCache() { + if (activity == null) return + val files = chapterCache.cacheDir.listFiles() ?: return + + var deletedFiles = 0 + + val ctrl = DeletingFilesDialogController() + ctrl.total = files.size + ctrl.showDialog(router) + + Observable.defer { Observable.from(files) } + .doOnNext { file -> + if (chapterCache.removeFileFromCache(file.name)) { + deletedFiles++ + } + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + ctrl.setProgress(deletedFiles) + }, { + activity?.toast(R.string.cache_delete_error) + }, { + ctrl.finish() + activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles)) + findPreference(CLEAR_CACHE_KEY)?.summary = + resources?.getString(R.string.used_cache, chapterCache.readableSize) + }) + } + + class DeletingFilesDialogController : DialogController() { + + var total = 0 + + private var materialDialog: MaterialDialog? = null + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.deleting) + .progress(false, total, true) + .cancelable(false) + .build() + .also { materialDialog = it } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + materialDialog = null + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + finish() + } + + fun setProgress(deletedFiles: Int) { + materialDialog?.setProgress(deletedFiles) + } + + fun finish() { + router.popController(this) + } + } + + class ClearDatabaseDialogController : DialogController() { + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .content(R.string.clear_database_confirmation) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + (targetController as? SettingsAdvancedController)?.clearDatabase() + } + .build() + } + } + + private fun clearDatabase() { + // Avoid weird behavior by going back to the library. + val newBackstack = listOf(RouterTransaction.with(LibraryController())) + + router.backstack.drop(1) + + router.setBackstack(newBackstack, FadeChangeHandler()) + + db.deleteMangasNotInLibrary().executeAsBlocking() + db.deleteHistoryNoLastRead().executeAsBlocking() + activity?.toast(R.string.clear_database_completed) + } + + private companion object { + const val CLEAR_CACHE_KEY = "pref_clear_cache_key" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt deleted file mode 100755 index e8576414d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt +++ /dev/null @@ -1,117 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.os.Bundle -import android.support.v7.preference.Preference -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.cache.ChapterCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.util.plusAssign -import eu.kanade.tachiyomi.util.toast -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy -import java.util.concurrent.atomic.AtomicInteger - -class SettingsAdvancedFragment : SettingsFragment() { - - companion object { - fun newInstance(rootKey: String): SettingsAdvancedFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsAdvancedFragment().apply { arguments = args } - } - } - - private val network: NetworkHelper by injectLazy() - - private val chapterCache: ChapterCache by injectLazy() - - private val db: DatabaseHelper by injectLazy() - - private val clearCache: Preference by bindPref(R.string.pref_clear_chapter_cache_key) - - private val clearDatabase: Preference by bindPref(R.string.pref_clear_database_key) - - private val clearCookies: Preference by bindPref(R.string.pref_clear_cookies_key) - - private val refreshMetadata: Preference by bindPref(R.string.pref_refresh_library_metadata_key) - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - clearCache.setOnPreferenceClickListener { - clearChapterCache() - true - } - clearCache.summary = getString(R.string.used_cache, chapterCache.readableSize) - - clearCookies.setOnPreferenceClickListener { - network.cookies.removeAll() - activity.toast(R.string.cookies_cleared) - true - } - - clearDatabase.setOnPreferenceClickListener { - clearDatabase() - true - } - - refreshMetadata.setOnPreferenceClickListener { - LibraryUpdateService.start(context, details = true) - true - } - } - - private fun clearChapterCache() { - val deletedFiles = AtomicInteger() - - val files = chapterCache.cacheDir.listFiles() ?: return - - val dialog = MaterialDialog.Builder(activity) - .title(R.string.deleting) - .progress(false, files.size, true) - .cancelable(false) - .show() - - subscriptions += Observable.defer { Observable.from(files) } - .concatMap { file -> - if (chapterCache.removeFileFromCache(file.name)) { - deletedFiles.incrementAndGet() - } - Observable.just(file) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - dialog.incrementProgress(1) - }, { - dialog.dismiss() - activity.toast(R.string.cache_delete_error) - }, { - dialog.dismiss() - activity.toast(getString(R.string.cache_deleted, deletedFiles.get())) - clearCache.summary = getString(R.string.used_cache, chapterCache.readableSize) - }) - } - - private fun clearDatabase() { - MaterialDialog.Builder(activity) - .content(R.string.clear_database_confirmation) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { dialog, which -> - (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_DATABASE_CLEARED - db.deleteMangasNotInLibrary().executeAsBlocking() - db.deleteHistoryNoLastRead().executeAsBlocking() - activity.toast(R.string.clear_database_completed) - } - .show() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt new file mode 100644 index 000000000..6c1994835 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt @@ -0,0 +1,462 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.Manifest.permission.READ_EXTERNAL_STORAGE +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.app.Activity +import android.app.Dialog +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.support.v7.preference.PreferenceScreen +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import com.hippo.unifile.UniFile +import com.nononsenseapps.filepicker.FilePickerActivity +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.backup.BackupConst +import eu.kanade.tachiyomi.data.backup.BackupCreateService +import eu.kanade.tachiyomi.data.backup.BackupCreatorJob +import eu.kanade.tachiyomi.data.backup.BackupRestoreService +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag +import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe +import eu.kanade.tachiyomi.util.getUriCompat +import eu.kanade.tachiyomi.util.registerLocalReceiver +import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.util.unregisterLocalReceiver +import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.util.concurrent.TimeUnit +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsBackupController : SettingsController() { + + /** + * Flags containing information of what to backup. + */ + private var backupFlags = 0 + + private val receiver = BackupBroadcastReceiver() + + init { + preferences.context.registerLocalReceiver(receiver, IntentFilter(BackupConst.INTENT_FILTER)) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 500) + } + + override fun onDestroy() { + super.onDestroy() + preferences.context.unregisterLocalReceiver(receiver) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.backup + + preference { + titleRes = R.string.pref_create_backup + summaryRes = R.string.pref_create_backup_summ + + onClick { + val ctrl = CreateBackupDialog() + ctrl.targetController = this@SettingsBackupController + ctrl.showDialog(router) + } + } + preference { + titleRes = R.string.pref_restore_backup + summaryRes = R.string.pref_restore_backup_summ + + onClick { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "application/*" + val title = resources?.getString(R.string.file_select_backup) + val chooser = Intent.createChooser(intent, title) + startActivityForResult(chooser, CODE_BACKUP_RESTORE) + } + } + preferenceCategory { + titleRes = R.string.pref_backup_service_category + + intListPreference { + key = Keys.backupInterval + titleRes = R.string.pref_backup_interval + entriesRes = arrayOf(R.string.update_never, R.string.update_6hour, + R.string.update_12hour, R.string.update_24hour, + R.string.update_48hour, R.string.update_weekly) + entryValues = arrayOf("0", "6", "12", "24", "48", "168") + defaultValue = "0" + summary = "%s" + + onChange { newValue -> + // Always cancel the previous task, it seems that sometimes they are not updated + BackupCreatorJob.cancelTask() + + val interval = (newValue as String).toInt() + if (interval > 0) { + BackupCreatorJob.setupTask(interval) + } + true + } + } + val backupDir = preference { + key = Keys.backupDirectory + titleRes = R.string.pref_backup_directory + + onClick { + val currentDir = preferences.backupsDirectory().getOrDefault() + + val intent = if (Build.VERSION.SDK_INT < 21) { + // Custom dir selected, open directory selector + val i = Intent(activity, CustomLayoutPickerActivity::class.java) + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) + i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) + + } else { + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + } + startActivityForResult(intent, CODE_BACKUP_DIR) + } + + preferences.backupsDirectory().asObservable() + .subscribeUntilDestroy { path -> + val dir = UniFile.fromUri(context, Uri.parse(path)) + summary = dir.filePath ?: path + } + } + val backupNumber = intListPreference { + key = Keys.numberOfBackups + titleRes = R.string.pref_backup_slots + entries = arrayOf("1", "2", "3", "4", "5") + entryValues = entries + defaultValue = "1" + summary = "%s" + } + + preferences.backupInterval().asObservable() + .subscribeUntilDestroy { + backupDir.isVisible = it > 0 + backupNumber.isVisible = it > 0 + } + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + CODE_BACKUP_DIR -> if (data != null && resultCode == Activity.RESULT_OK) { + val activity = activity ?: return + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + val uri = Uri.fromFile(File(data.data.path)) + preferences.backupsDirectory().set(uri.toString()) + } else { + val uri = data.data + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + activity.contentResolver.takePersistableUriPermission(uri, flags) + + val file = UniFile.fromUri(activity, uri) + preferences.backupsDirectory().set(file.uri.toString()) + } + } + CODE_BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) { + val activity = activity ?: return + val uri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + val dir = data.data.path + val file = File(dir, Backup.getDefaultFilename()) + + Uri.fromFile(file) + } else { + val uri = data.data + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + activity.contentResolver.takePersistableUriPermission(uri, flags) + val file = UniFile.fromUri(activity, uri) + + file.uri + } + + CreatingBackupDialog().showDialog(router, TAG_CREATING_BACKUP_DIALOG) + BackupCreateService.makeBackup(activity, uri, backupFlags) + } + CODE_BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) { + val uri = data.data + RestoreBackupDialog(uri).showDialog(router) + } + } + } + + fun createBackup(flags: Int) { + backupFlags = flags + + // If API lower as KitKat use custom dir picker + val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // Get dirs + val preferences: PreferencesHelper = Injekt.get() + val currentDir = preferences.backupsDirectory().getOrDefault() + + Intent(activity, CustomLayoutPickerActivity::class.java) + .putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) + .putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) + } else { + // Use Androids build in file creator + Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("application/*") + .putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename()) + } + startActivityForResult(intent, CODE_BACKUP_CREATE) + } + + class CreateBackupDialog : DialogController() { + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val options = arrayOf(R.string.manga, R.string.categories, R.string.chapters, + R.string.track, R.string.history) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.pref_create_backup) + .content(R.string.backup_choice) + .items(options) + .itemsDisabledIndices(0) + .itemsCallbackMultiChoice(arrayOf(0, 1, 2, 3, 4), { _, positions, _ -> + var flags = 0 + for (i in 1..positions.size - 1) { + when (positions[i]) { + 1 -> flags = flags or BackupCreateService.BACKUP_CATEGORY + 2 -> flags = flags or BackupCreateService.BACKUP_CHAPTER + 3 -> flags = flags or BackupCreateService.BACKUP_TRACK + 4 -> flags = flags or BackupCreateService.BACKUP_HISTORY + } + } + + (targetController as? SettingsBackupController)?.createBackup(flags) + true + }) + .positiveText(R.string.action_create) + .negativeText(android.R.string.cancel) + .build() + } + } + + class CreatingBackupDialog : DialogController() { + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.backup) + .content(R.string.creating_backup) + .progress(true, 0) + .cancelable(false) + .build() + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + router.popController(this) + } + } + + class CreatedBackupDialog(bundle: Bundle? = null) : DialogController(bundle) { + constructor(uri: Uri) : this(Bundle().apply { + putParcelable(KEY_URI, uri) + }) + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val unifile = UniFile.fromUri(activity, args.getParcelable(KEY_URI)) + return MaterialDialog.Builder(activity) + .title(R.string.backup_created) + .content(activity.getString(R.string.file_saved, unifile.filePath)) + .positiveText(R.string.action_close) + .negativeText(R.string.action_export) + .onNegative { _, _ -> + val sendIntent = Intent(Intent.ACTION_SEND) + sendIntent.type = "application/json" + sendIntent.putExtra(Intent.EXTRA_STREAM, unifile.uri) + startActivity(Intent.createChooser(sendIntent, "")) + } + .build() + } + + private companion object { + const val KEY_URI = "BackupCreatedDialog.uri" + } + } + + class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) { + constructor(uri: Uri) : this(Bundle().apply { + putParcelable(KEY_URI, uri) + }) + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.pref_restore_backup) + .content(R.string.backup_restore_content) + .positiveText(R.string.action_restore) + .onPositive { _, _ -> + val context = applicationContext + if (context != null) { + RestoringBackupDialog().showDialog(router, TAG_RESTORING_BACKUP_DIALOG) + BackupRestoreService.start(context, args.getParcelable(KEY_URI)) + } + } + .build() + } + + private companion object { + const val KEY_URI = "RestoreBackupDialog.uri" + } + } + + class RestoringBackupDialog : DialogController() { + private var materialDialog: MaterialDialog? = null + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .title(R.string.backup) + .content(R.string.restoring_backup) + .progress(false, 100, true) + .cancelable(false) + .negativeText(R.string.action_stop) + .onNegative { _, _ -> + applicationContext?.let { BackupRestoreService.stop(it) } + } + .build() + .also { materialDialog = it } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + materialDialog = null + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + router.popController(this) + } + + fun updateProgress(content: String?, progress: Int, amount: Int) { + val dialog = materialDialog ?: return + dialog.setContent(content) + dialog.setProgress(progress) + dialog.maxProgress = amount + } + } + + class RestoredBackupDialog(bundle: Bundle? = null) : DialogController(bundle) { + constructor(time: Long, errorCount: Int, path: String, file: String) : this(Bundle().apply { + putLong(KEY_TIME, time) + putInt(KEY_ERROR_COUNT, errorCount) + putString(KEY_PATH, path) + putString(KEY_FILE, file) + }) + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val time = args.getLong(KEY_TIME) + val errors = args.getInt(KEY_ERROR_COUNT) + val path = args.getString(KEY_PATH) + val file = args.getString(KEY_FILE) + val timeString = String.format("%02d min, %02d sec", + TimeUnit.MILLISECONDS.toMinutes(time), + TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes(time)) + ) + + return MaterialDialog.Builder(activity) + .title(R.string.restore_completed) + .content(activity.getString(R.string.restore_completed_content, timeString, + if (errors > 0) "$errors" else activity.getString(android.R.string.no))) + .positiveText(R.string.action_close) + .negativeText(R.string.action_open_log) + .onNegative { _, _ -> + val context = applicationContext ?: return@onNegative + if (!path.isEmpty()) { + val destFile = File(path, file) + val uri = destFile.getUriCompat(context) + val sendIntent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "text/plain") + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_GRANT_READ_URI_PERMISSION + } + startActivity(sendIntent) + } else { + context.toast(context.getString(R.string.error_opening_log)) + } + } + .build() + } + + private companion object { + const val KEY_TIME = "RestoredBackupDialog.time" + const val KEY_ERROR_COUNT = "RestoredBackupDialog.errors" + const val KEY_PATH = "RestoredBackupDialog.path" + const val KEY_FILE = "RestoredBackupDialog.file" + } + } + + inner class BackupBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.getStringExtra(BackupConst.ACTION)) { + BackupConst.ACTION_BACKUP_COMPLETED_DIALOG -> { + router.popControllerWithTag(TAG_CREATING_BACKUP_DIALOG) + val uri = Uri.parse(intent.getStringExtra(BackupConst.EXTRA_URI)) + CreatedBackupDialog(uri).showDialog(router) + } + BackupConst.ACTION_SET_PROGRESS_DIALOG -> { + val progress = intent.getIntExtra(BackupConst.EXTRA_PROGRESS, 0) + val amount = intent.getIntExtra(BackupConst.EXTRA_AMOUNT, 0) + val content = intent.getStringExtra(BackupConst.EXTRA_CONTENT) + (router.getControllerWithTag(TAG_RESTORING_BACKUP_DIALOG) + as? RestoringBackupDialog)?.updateProgress(content, progress, amount) + } + BackupConst.ACTION_RESTORE_COMPLETED_DIALOG -> { + router.popControllerWithTag(TAG_RESTORING_BACKUP_DIALOG) + val time = intent.getLongExtra(BackupConst.EXTRA_TIME, 0) + val errors = intent.getIntExtra(BackupConst.EXTRA_ERRORS, 0) + val path = intent.getStringExtra(BackupConst.EXTRA_ERROR_FILE_PATH) + val file = intent.getStringExtra(BackupConst.EXTRA_ERROR_FILE) + if (errors > 0) { + RestoredBackupDialog(time, errors, path, file).showDialog(router) + } + } + BackupConst.ACTION_ERROR_BACKUP_DIALOG -> { + router.popControllerWithTag(TAG_CREATING_BACKUP_DIALOG) + context.toast(intent.getStringExtra(BackupConst.EXTRA_ERROR_MESSAGE)) + } + BackupConst.ACTION_ERROR_RESTORE_DIALOG -> { + router.popControllerWithTag(TAG_RESTORING_BACKUP_DIALOG) + context.toast(intent.getStringExtra(BackupConst.EXTRA_ERROR_MESSAGE)) + } + } + } + } + + private companion object { + const val CODE_BACKUP_CREATE = 501 + const val CODE_BACKUP_RESTORE = 502 + const val CODE_BACKUP_DIR = 503 + + const val TAG_CREATING_BACKUP_DIALOG = "CreatingBackupDialog" + const val TAG_RESTORING_BACKUP_DIALOG = "RestoringBackupDialog" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupFragment.kt deleted file mode 100644 index 7ef9a70cd..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupFragment.kt +++ /dev/null @@ -1,407 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.app.Activity -import android.app.Dialog -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import com.hippo.unifile.UniFile -import com.nononsenseapps.filepicker.FilePickerActivity -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupCreateService -import eu.kanade.tachiyomi.data.backup.BackupCreatorJob -import eu.kanade.tachiyomi.data.backup.BackupRestoreService -import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import eu.kanade.tachiyomi.util.* -import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity -import eu.kanade.tachiyomi.widget.preference.IntListPreference -import net.xpece.android.support.preference.Preference -import rx.subscriptions.Subscriptions -import uy.kohesive.injekt.injectLazy -import java.io.File -import java.util.concurrent.TimeUnit -import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID - -/** - * Settings for [BackupCreateService] and [BackupRestoreService] - */ -class SettingsBackupFragment : SettingsFragment() { - - companion object { - const val INTENT_FILTER = "SettingsBackupFragment" - const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG" - const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG" - const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG" - const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG" - const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG" - const val ACTION = "$ID.$INTENT_FILTER.ACTION" - const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS" - const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT" - const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS" - const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT" - const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE" - const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI" - const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME" - const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH" - const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE" - - private const val BACKUP_CREATE = 201 - private const val BACKUP_RESTORE = 202 - private const val BACKUP_DIR = 203 - - fun newInstance(rootKey: String): SettingsBackupFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsBackupFragment().apply { arguments = args } - } - } - - /** - * Preference selected to create backup - */ - private val createBackup: Preference by bindPref(R.string.pref_create_local_backup_key) - - /** - * Preference selected to restore backup - */ - private val restoreBackup: Preference by bindPref(R.string.pref_restore_local_backup_key) - - /** - * Preference which determines the frequency of automatic backups. - */ - private val automaticBackup: IntListPreference by bindPref(R.string.pref_backup_interval_key) - - /** - * Preference containing number of automatic backups - */ - private val backupSlots: IntListPreference by bindPref(R.string.pref_backup_slots_key) - - /** - * Preference containing interval of automatic backups - */ - private val backupDirPref: Preference by bindPref(R.string.pref_backup_directory_key) - - /** - * Preferences - */ - private val preferences: PreferencesHelper by injectLazy() - - /** - * Value containing information on what to backup - */ - private var backup_flags = 0 - - /** - * The root directory for backups.. - */ - private var backupDir = preferences.backupsDirectory().getOrDefault().let { - UniFile.fromUri(context, Uri.parse(it)) - } - - val restoreDialog: MaterialDialog by lazy { - MaterialDialog.Builder(context) - .title(R.string.backup) - .content(R.string.restoring_backup) - .progress(false, 100, true) - .cancelable(false) - .negativeText(R.string.action_stop) - .onNegative { materialDialog, _ -> - BackupRestoreService.stop(context) - materialDialog.dismiss() - } - .build() - } - - val backupDialog: MaterialDialog by lazy { - MaterialDialog.Builder(context) - .title(R.string.backup) - .content(R.string.creating_backup) - .progress(true, 0) - .cancelable(false) - .build() - } - - private val receiver = object : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - when (intent.getStringExtra(ACTION)) { - ACTION_BACKUP_COMPLETED_DIALOG -> { - backupDialog.dismiss() - val uri = Uri.parse(intent.getStringExtra(EXTRA_URI)) - val file = UniFile.fromUri(context, uri) - MaterialDialog.Builder(this@SettingsBackupFragment.context) - .title(getString(R.string.backup_created)) - .content(getString(R.string.file_saved, file.filePath)) - .positiveText(getString(R.string.action_close)) - .negativeText(getString(R.string.action_export)) - .onPositive { materialDialog, _ -> materialDialog.dismiss() } - .onNegative { _, _ -> - val sendIntent = Intent(Intent.ACTION_SEND) - sendIntent.type = "application/json" - sendIntent.putExtra(Intent.EXTRA_STREAM, file.uri) - startActivity(Intent.createChooser(sendIntent, "")) - } - .safeShow() - - } - ACTION_SET_PROGRESS_DIALOG -> { - val progress = intent.getIntExtra(EXTRA_PROGRESS, 0) - val amount = intent.getIntExtra(EXTRA_AMOUNT, 0) - val content = intent.getStringExtra(EXTRA_CONTENT) - restoreDialog.setContent(content) - restoreDialog.setProgress(progress) - restoreDialog.maxProgress = amount - } - ACTION_RESTORE_COMPLETED_DIALOG -> { - restoreDialog.dismiss() - val time = intent.getLongExtra(EXTRA_TIME, 0) - val errors = intent.getIntExtra(EXTRA_ERRORS, 0) - val path = intent.getStringExtra(EXTRA_ERROR_FILE_PATH) - val file = intent.getStringExtra(EXTRA_ERROR_FILE) - val timeString = String.format("%02d min, %02d sec", - TimeUnit.MILLISECONDS.toMinutes(time), - TimeUnit.MILLISECONDS.toSeconds(time) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time)) - ) - - if (errors > 0) { - MaterialDialog.Builder(this@SettingsBackupFragment.context) - .title(getString(R.string.restore_completed)) - .content(getString(R.string.restore_completed_content, timeString, - if (errors > 0) "$errors" else getString(android.R.string.no))) - .positiveText(getString(R.string.action_close)) - .negativeText(getString(R.string.action_open_log)) - .onPositive { materialDialog, _ -> materialDialog.dismiss() } - .onNegative { materialDialog, _ -> - if (!path.isEmpty()) { - val destFile = File(path, file) - val uri = destFile.getUriCompat(context) - val sendIntent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, "text/plain") - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION - } - startActivity(sendIntent) - } else { - context.toast(getString(R.string.error_opening_log)) - } - materialDialog.dismiss() - } - .safeShow() - } - } - ACTION_ERROR_BACKUP_DIALOG -> { - context.toast(intent.getStringExtra(EXTRA_ERROR_MESSAGE)) - backupDialog.dismiss() - } - ACTION_ERROR_RESTORE_DIALOG -> { - context.toast(intent.getStringExtra(EXTRA_ERROR_MESSAGE)) - restoreDialog.dismiss() - } - } - } - - } - - override fun onStart() { - super.onStart() - context.registerLocalReceiver(receiver, IntentFilter(INTENT_FILTER)) - } - - override fun onPause() { - context.unregisterLocalReceiver(receiver) - super.onPause() - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - if (savedState != null) { - if (BackupRestoreService.isRunning(context)) { - restoreDialog.safeShow() - } - else if (BackupCreateService.isRunning(context)) { - backupDialog.safeShow() - } - } - - (activity as BaseActivity).requestPermissionsOnMarshmallow() - - // Set onClickListeners - createBackup.setOnPreferenceClickListener { - MaterialDialog.Builder(context) - .title(R.string.pref_create_backup) - .content(R.string.backup_choice) - .items(R.array.backup_options) - .itemsCallbackMultiChoice(arrayOf(0, 1, 2, 3, 4 /*todo not hard code*/)) { _, positions, _ -> - // TODO not very happy with global value, but putExtra doesn't work - backup_flags = 0 - for (i in 1..positions.size - 1) { - when (positions[i]) { - 1 -> backup_flags = backup_flags or BackupCreateService.BACKUP_CATEGORY - 2 -> backup_flags = backup_flags or BackupCreateService.BACKUP_CHAPTER - 3 -> backup_flags = backup_flags or BackupCreateService.BACKUP_TRACK - 4 -> backup_flags = backup_flags or BackupCreateService.BACKUP_HISTORY - } - } - // If API lower as KitKat use custom dir picker - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - // Get dirs - val currentDir = preferences.backupsDirectory().getOrDefault() - - val i = Intent(activity, CustomLayoutPickerActivity::class.java) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) - i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) - startActivityForResult(i, BACKUP_CREATE) - } else { - // Use Androids build in file creator - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - - // TODO create custom MIME data type? Will make older backups deprecated - intent.type = "application/*" - intent.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename()) - startActivityForResult(intent, BACKUP_CREATE) - } - true - } - .itemsDisabledIndices(0) - .positiveText(getString(R.string.action_create)) - .negativeText(android.R.string.cancel) - .safeShow() - true - } - - restoreBackup.setOnPreferenceClickListener { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "application/*" - val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup)) - startActivityForResult(chooser, BACKUP_RESTORE) - true - } - - automaticBackup.setOnPreferenceChangeListener { _, newValue -> - // Always cancel the previous task, it seems that sometimes they are not updated. - BackupCreatorJob.cancelTask() - - val interval = (newValue as String).toInt() - if (interval > 0) { - BackupCreatorJob.setupTask(interval) - } - true - } - - backupSlots.setOnPreferenceChangeListener { preference, newValue -> - preferences.numberOfBackups().set((newValue as String).toInt()) - preference.summary = newValue - true - } - - backupDirPref.setOnPreferenceClickListener { - val currentDir = preferences.backupsDirectory().getOrDefault() - - if (Build.VERSION.SDK_INT < 21) { - // Custom dir selected, open directory selector - val i = Intent(activity, CustomLayoutPickerActivity::class.java) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) - i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) - - startActivityForResult(i, BACKUP_DIR) - } else { - val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - startActivityForResult(i, BACKUP_DIR) - } - - true - } - - subscriptions += preferences.backupsDirectory().asObservable() - .subscribe { path -> - backupDir = UniFile.fromUri(context, Uri.parse(path)) - backupDirPref.summary = backupDir.filePath ?: path - } - - subscriptions += preferences.backupInterval().asObservable() - .subscribe { - backupDirPref.isVisible = it > 0 - backupSlots.isVisible = it > 0 - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - BACKUP_DIR -> if (data != null && resultCode == Activity.RESULT_OK) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - val uri = Uri.fromFile(File(data.data.path)) - preferences.backupsDirectory().set(uri.toString()) - } else { - val uri = data.data - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - context.contentResolver.takePersistableUriPermission(uri, flags) - - val file = UniFile.fromUri(context, uri) - preferences.backupsDirectory().set(file.uri.toString()) - } - } - BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - val dir = data.data.path - val file = File(dir, Backup.getDefaultFilename()) - - backupDialog.safeShow() - BackupCreateService.makeBackup(context, file.toURI().toString(), backup_flags) - } else { - val uri = data.data - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - context.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(context, uri) - - backupDialog.safeShow() - BackupCreateService.makeBackup(context, file.uri.toString(), backup_flags) - } - } - BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) { - val uri = data.data - - MaterialDialog.Builder(context) - .title(getString(R.string.pref_restore_backup)) - .content(getString(R.string.backup_restore_content)) - .positiveText(getString(R.string.action_restore)) - .onPositive { _, _ -> - restoreDialog.safeShow() - BackupRestoreService.start(context, uri) - } - .safeShow() - } - } - } - - fun MaterialDialog.Builder.safeShow(): Dialog { - return build().safeShow() - } - - fun Dialog.safeShow(): Dialog { - subscriptions += Subscriptions.create { dismiss() } - show() - return this - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt new file mode 100644 index 000000000..f78a4c98c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt @@ -0,0 +1,70 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.content.Context +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.preference.PreferenceController +import android.support.v7.preference.PreferenceScreen +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import rx.Observable +import rx.Subscription +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +abstract class SettingsController : PreferenceController() { + + val preferences: PreferencesHelper = Injekt.get() + + var untilDestroySubscriptions = CompositeSubscription() + private set + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View { + if (untilDestroySubscriptions.isUnsubscribed) { + untilDestroySubscriptions = CompositeSubscription() + } + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + untilDestroySubscriptions.unsubscribe() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val screen = preferenceManager.createPreferenceScreen(getThemedContext()) + preferenceScreen = screen + setupPreferenceScreen(screen) + } + + abstract fun setupPreferenceScreen(screen: PreferenceScreen): Any? + + private fun getThemedContext(): Context { + val tv = TypedValue() + activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true) + return ContextThemeWrapper(activity, tv.resourceId) + } + + open fun getTitle(): String? { + return preferenceScreen?.title?.toString() + } + + override fun onAttach(view: View) { + (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() + super.onAttach(view) + } + + fun Observable.subscribeUntilDestroy(): Subscription { + return subscribe().also { untilDestroySubscriptions.add(it) } + } + + fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { + return subscribe(onNext).also { untilDestroySubscriptions.add(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt new file mode 100644 index 000000000..39485d6ec --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -0,0 +1,205 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.app.Activity +import android.app.Dialog +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.support.v4.content.ContextCompat +import android.support.v7.preference.PreferenceScreen +import com.afollestad.materialdialogs.MaterialDialog +import com.hippo.unifile.UniFile +import com.nononsenseapps.filepicker.FilePickerActivity +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.DiskUtil +import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.io.File +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsDownloadController : SettingsController() { + + private val db: DatabaseHelper by injectLazy() + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_downloads + + preference { + key = Keys.downloadsDirectory + titleRes = R.string.pref_download_directory + onClick { + val ctrl = DownloadDirectoriesDialog() + ctrl.targetController = this@SettingsDownloadController + ctrl.showDialog(router) + } + + preferences.downloadsDirectory().asObservable() + .subscribeUntilDestroy { path -> + val dir = UniFile.fromUri(context, Uri.parse(path)) + summary = dir.filePath ?: path + + // Don't display downloaded chapters in gallery apps creating .nomedia + if (dir != null && dir.exists()) { + val nomedia = dir.findFile(".nomedia") + if (nomedia == null) { + dir.createFile(".nomedia") + applicationContext?.let { DiskUtil.scanMedia(it, dir.uri) } + } + } + } + } + switchPreference { + key = Keys.downloadOnlyOverWifi + titleRes = R.string.pref_download_only_over_wifi + defaultValue = true + } + intListPreference { + key = Keys.downloadThreads + titleRes = R.string.pref_download_slots + entries = arrayOf("1", "2", "3") + entryValues = arrayOf("1", "2", "3") + defaultValue = "1" + summary = "%s" + } + preferenceCategory { + titleRes = R.string.pref_remove_after_read + + switchPreference { + key = Keys.removeAfterMarkedAsRead + titleRes = R.string.pref_remove_after_marked_as_read + defaultValue = false + } + intListPreference { + key = Keys.removeAfterReadSlots + titleRes = R.string.pref_remove_after_read + entriesRes = arrayOf(R.string.disabled, R.string.last_read_chapter, + R.string.second_to_last, R.string.third_to_last, R.string.fourth_to_last, + R.string.fifth_to_last) + entryValues = arrayOf("-1", "0", "1", "2", "3", "4") + defaultValue = "-1" + summary = "%s" + } + } + + val dbCategories = db.getCategories().executeAsBlocking() + + preferenceCategory { + titleRes = R.string.pref_download_new + + switchPreference { + key = Keys.downloadNew + titleRes = R.string.pref_download_new + defaultValue = false + } + multiSelectListPreference { + key = Keys.downloadNewCategories + titleRes = R.string.pref_download_new_categories + entries = dbCategories.map { it.name }.toTypedArray() + entryValues = dbCategories.map { it.id.toString() }.toTypedArray() + + preferences.downloadNew().asObservable() + .subscribeUntilDestroy { isVisible = it } + + preferences.downloadNewCategories().asObservable() + .subscribeUntilDestroy { + val selectedCategories = it + .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } } + .sortedBy { it.order } + + summary = if (selectedCategories.isEmpty()) + resources?.getString(R.string.all) + else + selectedCategories.joinToString { it.name } + } + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + DOWNLOAD_DIR_PRE_L -> if (data != null && resultCode == Activity.RESULT_OK) { + val uri = Uri.fromFile(File(data.data.path)) + preferences.downloadsDirectory().set(uri.toString()) + } + DOWNLOAD_DIR_L -> if (data != null && resultCode == Activity.RESULT_OK) { + val context = applicationContext ?: return + val uri = data.data + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + @Suppress("NewApi") + context.contentResolver.takePersistableUriPermission(uri, flags) + + val file = UniFile.fromUri(context, uri) + preferences.downloadsDirectory().set(file.uri.toString()) + } + } + } + + fun predefinedDirectorySelected(selectedDir: String) { + val path = Uri.fromFile(File(selectedDir)) + preferences.downloadsDirectory().set(path.toString()) + } + + fun customDirectorySelected(currentDir: String) { + if (Build.VERSION.SDK_INT < 21) { + val i = Intent(activity, CustomLayoutPickerActivity::class.java) + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) + i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) + + startActivityForResult(i, DOWNLOAD_DIR_PRE_L) + } else { + val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(i, DOWNLOAD_DIR_L) + } + } + + class DownloadDirectoriesDialog : DialogController() { + + private val preferences: PreferencesHelper = Injekt.get() + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val currentDir = preferences.downloadsDirectory().getOrDefault() + val externalDirs = getExternalDirs() + File(activity.getString(R.string.custom_dir)) + val selectedIndex = externalDirs.map(File::toString).indexOfFirst { it in currentDir } + + return MaterialDialog.Builder(activity) + .items(externalDirs) + .itemsCallbackSingleChoice(selectedIndex, { _, _, which, text -> + val target = targetController as? SettingsDownloadController + if (which == externalDirs.lastIndex) { + target?.customDirectorySelected(currentDir) + } else { + target?.predefinedDirectorySelected(text.toString()) + } + true + }) + .build() + } + + private fun getExternalDirs(): List { + val defaultDir = Environment.getExternalStorageDirectory().absolutePath + + File.separator + resources?.getString(R.string.app_name) + + File.separator + "downloads" + + return mutableListOf(File(defaultDir)) + + ContextCompat.getExternalFilesDirs(activity, "").filterNotNull() + } + } + + private companion object { + const val DOWNLOAD_DIR_PRE_L = 103 + const val DOWNLOAD_DIR_L = 104 + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt deleted file mode 100755 index 09d56d598..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt +++ /dev/null @@ -1,149 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Environment -import android.support.v4.content.ContextCompat -import android.support.v7.preference.Preference -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import com.hippo.unifile.UniFile -import com.nononsenseapps.filepicker.FilePickerActivity -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.util.plusAssign -import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity -import net.xpece.android.support.preference.MultiSelectListPreference -import uy.kohesive.injekt.injectLazy -import java.io.File - -class SettingsDownloadsFragment : SettingsFragment() { - - companion object { - const val DOWNLOAD_DIR_PRE_L = 103 - const val DOWNLOAD_DIR_L = 104 - - fun newInstance(rootKey: String): SettingsDownloadsFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsDownloadsFragment().apply { arguments = args } - } - } - - private val preferences: PreferencesHelper by injectLazy() - - private val db: DatabaseHelper by injectLazy() - - val downloadDirPref: Preference by bindPref(R.string.pref_download_directory_key) - - val downloadCategory: MultiSelectListPreference by bindPref(R.string.pref_download_new_categories_key) - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - downloadDirPref.setOnPreferenceClickListener { - - val currentDir = preferences.downloadsDirectory().getOrDefault() - val externalDirs = getExternalFilesDirs() + File(getString(R.string.custom_dir)) - val selectedIndex = externalDirs.map(File::toString).indexOfFirst { it in currentDir } - - MaterialDialog.Builder(activity) - .items(externalDirs) - .itemsCallbackSingleChoice(selectedIndex, { dialog, view, which, text -> - if (which == externalDirs.lastIndex) { - if (Build.VERSION.SDK_INT < 21) { - // Custom dir selected, open directory selector - val i = Intent(activity, CustomLayoutPickerActivity::class.java) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) - i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) - - startActivityForResult(i, DOWNLOAD_DIR_PRE_L) - } else { - val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - startActivityForResult(i, DOWNLOAD_DIR_L) - } - } else { - // One of the predefined folders was selected - val path = Uri.fromFile(File(text.toString())) - preferences.downloadsDirectory().set(path.toString()) - } - true - }) - .show() - - true - } - - subscriptions += preferences.downloadsDirectory().asObservable() - .subscribe { path -> - val dir = UniFile.fromUri(context, Uri.parse(path)) - - downloadDirPref.summary = dir.filePath ?: path - - // Don't display downloaded chapters in gallery apps creating a ".nomedia" file. - if (dir != null && dir.exists()) { - dir.createFile(".nomedia") - } - } - - subscriptions += preferences.downloadNew().asObservable() - .subscribe { downloadCategory.isVisible = it } - - val dbCategories = db.getCategories().executeAsBlocking() - downloadCategory.apply { - entries = dbCategories.map { it.name }.toTypedArray() - entryValues = dbCategories.map { it.id.toString() }.toTypedArray() - } - - subscriptions += preferences.downloadNewCategories().asObservable() - .subscribe { - val selectedCategories = it - .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } } - .sortedBy { it.order } - - val summary = if (selectedCategories.isEmpty()) - getString(R.string.all) - else - selectedCategories.joinToString { it.name } - - downloadCategory.summary = summary - } - } - - fun getExternalFilesDirs(): List { - val defaultDir = Environment.getExternalStorageDirectory().absolutePath + - File.separator + getString(R.string.app_name) + - File.separator + "downloads" - - return mutableListOf(File(defaultDir)) + - ContextCompat.getExternalFilesDirs(activity, "").filterNotNull() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - DOWNLOAD_DIR_PRE_L -> if (data != null && resultCode == Activity.RESULT_OK) { - val uri = Uri.fromFile(File(data.data.path)) - preferences.downloadsDirectory().set(uri.toString()) - } - DOWNLOAD_DIR_L -> if (data != null && resultCode == Activity.RESULT_OK) { - val uri = data.data - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - @Suppress("NewApi") - context.contentResolver.takePersistableUriPermission(uri, flags) - - val file = UniFile.fromUri(context, uri) - preferences.downloadsDirectory().set(file.uri.toString()) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt new file mode 100755 index 000000000..63f1bbb24 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt @@ -0,0 +1,169 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.content.Intent +import android.os.Bundle +import android.support.v7.preference.PreferenceScreen +import android.view.View +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import exh.ui.migration.MetadataFetchDialog +import exh.ui.login.LoginActivity +import uy.kohesive.injekt.injectLazy + +/** + * EH Settings fragment + */ + +class SettingsEhFragment : SettingsController() { + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + title = "E-Hentai" + + switchPreference { + title = "Enable ExHentai" + summaryOff = "Requires login" + key = "enable_exhentai" + isPersistent = false + defaultValue = false + preferences.enableExhentai() + .asObservable().subscribeUntilDestroy { + isChecked = it + } + } + + switchPreference { + title = "Use Hentai@Home Network" + summary = "Do you wish to load images through the Hentai@Home Network? Disabling this option will reduce the amount of pages you are able to view" + key = "enable_hah" + defaultValue = "true" + } + + switchPreference { + title = "Show Japanese titles in search results" + summaryOn = "Currently showing Japanese titles in search results. Clear the chapter cache after changing this (in the Advanced section)" + summaryOff = "Currently showing English/Romanized titles in search results. Clear the chapter cache after changing this (in the Advanced section)" + key = "use_jp_title" + defaultValue = "false" + } + + switchPreference { + defaultValue = "true" + key = "secure_exh" + title = "Secure ExHentai/E-Hentai" + summary = "Use the HTTPS version of ExHentai/E-Hentai." + } + + listPreference { + defaultValue = "auto" + key = "ehentai_quality" + summary = "The quality of the downloaded images" + title = "Image quality" + entries = arrayOf( + "Auto", + "2400x", + "1600x", + "1280x", + "980x", + "780x" + ) + entryValues = arrayOf( + "auto", + "ovrs_2400", + "ovrs_1600", + "high", + "med", + "low" + ) + } + + listPreference { + title = "Search result count per page" + summary = "Requires the 'Paging Enlargement' hath perk" + defaultValue = "rc_0" + key = "ex_search_size" + entries = arrayOf( + "25 results", + "50 results", + "100 results", + "200 results" + ) + entryValues = arrayOf( + "rc_0", + "rc_1", + "rc_2", + "rc_3" + ) + dependency = "enable_exhentai" + } + + listPreference { + defaultValue = "tr_2" + title = "Thumbnail rows" + summary = "Affects loading speeds. It is recommended to set this to the maximum size your hath perks allow" + key = "ex_thumb_rows" + dependency = "enable_exhentai" + entries = arrayOf( + "4", + "10 (requires 'More Thumbs' hath perk)", + "20 (requires 'Thumbs Up' hath perk)", + "40 (requires 'All Thumbs' hath perk)" + ) + entryValues = arrayOf( + "tr_2", + "tr_5", + "tr_10", + "tr_20" + ) + } + + preferenceCategory { + title = "Advanced" + isPersistent = false + + preference { + title = "Migrate library metadata" + isPersistent = false + key = "ex_migrate_library" + summary = "Fetch the library metadata to enable tag searching in the library. This button will be visible even if you have already fetched the metadata" /> + } + } + } + + private val preferences: PreferencesHelper by injectLazy() + + val enableExhentaiPref by lazy { + findPreference("enable_exhentai") as SwitchPreference + } + + val migrateLibraryPref by lazy { + findPreference("ex_migrate_library") as Preference + } + + val useJpTitlePref by lazy { + findPreference("use_jp_title") as SwitchPreference + } + + override fun onViewCreated(view: View, savedState: Bundle?) { + super.onViewCreated(view, savedState) + + enableExhentaiPref.setOnPreferenceChangeListener { preference, newVal -> + newVal as Boolean + (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_EH_RECREATE + if(!newVal) { + preferences.enableExhentai().set(false) + true + } else { + startActivity(Intent(context, LoginActivity::class.java)) + false + } + } + + migrateLibraryPref.setOnPreferenceClickListener { + MetadataFetchDialog().askMigration(activity) + true + } + + useJpTitlePref.setOnPreferenceChangeListener { preference, any -> + (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_EH_RECREATE + true + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt deleted file mode 100755 index e7df22076..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt +++ /dev/null @@ -1,73 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.content.Intent -import android.os.Bundle -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.util.plusAssign -import exh.ui.migration.MetadataFetchDialog -import exh.ui.login.LoginActivity -import net.xpece.android.support.preference.Preference -import net.xpece.android.support.preference.SwitchPreference -import uy.kohesive.injekt.injectLazy - -/** - * EH Settings fragment - */ - -class SettingsEhFragment : SettingsFragment() { - companion object { - fun newInstance(rootKey: String): SettingsEhFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsEhFragment().apply { arguments = args } - } - } - - private val preferences: PreferencesHelper by injectLazy() - - val enableExhentaiPref by lazy { - findPreference("enable_exhentai") as SwitchPreference - } - - val migrateLibraryPref by lazy { - findPreference("ex_migrate_library") as Preference - } - - val useJpTitlePref by lazy { - findPreference("use_jp_title") as SwitchPreference - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - subscriptions += preferences - .enableExhentai() - .asObservable().subscribe { - enableExhentaiPref.isChecked = it - } - - enableExhentaiPref.setOnPreferenceChangeListener { preference, newVal -> - newVal as Boolean - (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_EH_RECREATE - if(!newVal) { - preferences.enableExhentai().set(false) - true - } else { - startActivity(Intent(context, LoginActivity::class.java)) - false - } - } - - migrateLibraryPref.setOnPreferenceClickListener { - MetadataFetchDialog().askMigration(activity) - true - } - - useJpTitlePref.setOnPreferenceChangeListener { preference, any -> - (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_EH_RECREATE - true - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt deleted file mode 100755 index 29bf38fad..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt +++ /dev/null @@ -1,63 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.os.Bundle -import android.support.annotation.CallSuper -import android.support.v7.preference.Preference -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.R -import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy -import rx.subscriptions.CompositeSubscription - -open class SettingsFragment : XpPreferenceFragment() { - - companion object { - fun newInstance(rootKey: String?): SettingsFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsFragment().apply { arguments = args } - } - } - - lateinit var subscriptions: CompositeSubscription - - override final fun onCreatePreferences2(savedState: Bundle?, rootKey: String?) { - subscriptions = CompositeSubscription() - - addPreferencesFromResource(R.xml.pref_general) - addPreferencesFromResource(R.xml.pref_reader) - addPreferencesFromResource(R.xml.pref_downloads) - addPreferencesFromResource(R.xml.pref_sources) - addPreferencesFromResource(R.xml.pref_tracking) - addPreferencesFromResource(R.xml.pref_backup) - addPreferencesFromResource(R.xml.eh_pref_eh) //EH - addPreferencesFromResource(R.xml.pref_advanced) - addPreferencesFromResource(R.xml.pref_about) - - // Setup root preference title. - preferenceScreen.title = activity.title - - PreferenceScreenNavigationStrategy.ReplaceFragment.onCreatePreferences(this, rootKey) - } - - @CallSuper - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - listView.isFocusable = false - } - - override fun onStart() { - super.onStart() - activity.title = preferenceScreen.title - } - - override fun onDestroyView() { - subscriptions.unsubscribe() - super.onDestroyView() - } - - protected inline fun bindPref(resId: Int): Lazy { - return lazy { findPreference(getString(resId)) as T } - } - -} \ No newline at end of file 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 new file mode 100644 index 000000000..5a755b4a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -0,0 +1,226 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.app.Dialog +import android.os.Bundle +import android.os.Handler +import android.support.v7.preference.PreferenceScreen +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.library.LibraryUpdateJob +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.LocaleHelper +import kotlinx.android.synthetic.main.pref_library_columns.view.* +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsGeneralController : SettingsController() { + + private val db: DatabaseHelper = Injekt.get() + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_general + + listPreference { + key = Keys.lang + titleRes = R.string.pref_language + entryValues = arrayOf("", "bg", "en", "es", "fr", "it", "lv", "nl", "pt", "pt-BR", "ru", + "vi") + entries = entryValues.map { value -> + val locale = LocaleHelper.getLocaleFromString(value.toString()) + locale?.getDisplayName(locale)?.capitalize() ?: + context.getString(R.string.system_default) + }.toTypedArray() + defaultValue = "" + summary = "%s" + + onChange { newValue -> + val activity = activity ?: return@onChange false + val app = activity.application + LocaleHelper.changeLocale(newValue.toString()) + LocaleHelper.updateConfiguration(app, app.resources.configuration) + activity.recreate() + true + } + } + intListPreference { + key = Keys.theme + titleRes = R.string.pref_theme + entriesRes = arrayOf(R.string.light_theme, R.string.dark_theme, R.string.amoled_theme) + entryValues = arrayOf("1", "2", "3") + defaultValue = "1" + summary = "%s" + + onChange { + activity?.recreate() + true + } + } + preference { + titleRes = R.string.pref_library_columns + onClick { + LibraryColumnsDialog().showDialog(router) + } + + fun getColumnValue(value: Int): String { + return if (value == 0) + context.getString(R.string.default_columns) + else + value.toString() + } + + Observable.combineLatest( + preferences.portraitColumns().asObservable(), + preferences.landscapeColumns().asObservable(), + { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) }) + .subscribeUntilDestroy { (portraitCols, landscapeCols) -> + val portrait = getColumnValue(portraitCols) + val landscape = getColumnValue(landscapeCols) + summary = "${context.getString(R.string.portrait)}: $portrait, " + + "${context.getString(R.string.landscape)}: $landscape" + } + } + intListPreference { + key = Keys.startScreen + titleRes = R.string.pref_start_screen + entriesRes = arrayOf(R.string.label_library, R.string.label_recent_manga, + R.string.label_recent_updates) + entryValues = arrayOf("1", "2", "3") + defaultValue = "1" + summary = "%s" + } + intListPreference { + key = Keys.libraryUpdateInterval + titleRes = R.string.pref_library_update_interval + entriesRes = arrayOf(R.string.update_never, R.string.update_1hour, + R.string.update_2hour, R.string.update_3hour, R.string.update_6hour, + R.string.update_12hour, R.string.update_24hour, R.string.update_48hour) + entryValues = arrayOf("0", "1", "2", "3", "6", "12", "24", "48") + defaultValue = "0" + summary = "%s" + + onChange { newValue -> + // Always cancel the previous task, it seems that sometimes they are not updated. + LibraryUpdateJob.cancelTask() + + val interval = (newValue as String).toInt() + if (interval > 0) { + LibraryUpdateJob.setupTask(interval) + } + true + } + } + multiSelectListPreference { + key = Keys.libraryUpdateRestriction + titleRes = R.string.pref_library_update_restriction + entriesRes = arrayOf(R.string.wifi, R.string.charging) + entryValues = arrayOf("wifi", "ac") + summaryRes = R.string.pref_library_update_restriction_summary + + preferences.libraryUpdateInterval().asObservable() + .subscribeUntilDestroy { isVisible = it > 0 } + + onChange { + // Post to event looper to allow the preference to be updated. + Handler().post { LibraryUpdateJob.setupTask() } + true + } + } + switchPreference { + key = Keys.updateOnlyNonCompleted + titleRes = R.string.pref_update_only_non_completed + defaultValue = false + } + + val dbCategories = db.getCategories().executeAsBlocking() + + multiSelectListPreference { + key = Keys.libraryUpdateCategories + titleRes = R.string.pref_library_update_categories + entries = dbCategories.map { it.name }.toTypedArray() + entryValues = dbCategories.map { it.id.toString() }.toTypedArray() + + preferences.libraryUpdateCategories().asObservable() + .subscribeUntilDestroy { + val selectedCategories = it + .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } } + .sortedBy { it.order } + + summary = if (selectedCategories.isEmpty()) + context.getString(R.string.all) + else + selectedCategories.joinToString { it.name } + } + } + intListPreference { + key = Keys.defaultCategory + titleRes = R.string.default_category + + val selectedCategory = dbCategories.find { it.id == preferences.defaultCategory() } + entries = arrayOf(context.getString(R.string.default_category_summary)) + + dbCategories.map { it.name }.toTypedArray() + entryValues = arrayOf("-1") + dbCategories.map { it.id.toString() }.toTypedArray() + defaultValue = "-1" + summary = selectedCategory?.name ?: context.getString(R.string.default_category_summary) + + onChange { newValue -> + summary = dbCategories.find { + it.id == (newValue as String).toInt() + }?.name ?: context.getString(R.string.default_category_summary) + true + } + } + } + + class LibraryColumnsDialog : DialogController() { + + private val preferences: PreferencesHelper = Injekt.get() + + private var portrait = preferences.portraitColumns().getOrDefault() + private var landscape = preferences.landscapeColumns().getOrDefault() + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.pref_library_columns) + .customView(R.layout.pref_library_columns, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> + preferences.portraitColumns().set(portrait) + preferences.landscapeColumns().set(landscape) + } + .build() + + onViewCreated(dialog.view) + return dialog + } + + fun onViewCreated(view: View) { + with(view.portrait_columns) { + displayedValues = arrayOf(context.getString(R.string.default_columns)) + + IntRange(1, 10).map(Int::toString) + value = portrait + + setOnValueChangedListener { _, _, newValue -> + portrait = newValue + } + } + with(view.landscape_columns) { + displayedValues = arrayOf(context.getString(R.string.default_columns)) + + IntRange(1, 10).map(Int::toString) + value = landscape + + setOnValueChangedListener { _, _, newValue -> + landscape = newValue + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt deleted file mode 100755 index 186bf112b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt +++ /dev/null @@ -1,167 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.os.Bundle -import android.support.v7.preference.Preference -import android.support.v7.preference.PreferenceFragmentCompat -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.util.LocaleHelper -import eu.kanade.tachiyomi.util.plusAssign -import eu.kanade.tachiyomi.widget.preference.IntListPreference -import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog -import eu.kanade.tachiyomi.widget.preference.SimpleDialogPreference -import net.xpece.android.support.preference.ListPreference -import net.xpece.android.support.preference.MultiSelectListPreference -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.injectLazy - -class SettingsGeneralFragment : SettingsFragment(), - PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback { - - - companion object { - fun newInstance(rootKey: String): SettingsGeneralFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsGeneralFragment().apply { arguments = args } - } - } - - private val preferences: PreferencesHelper by injectLazy() - - private val db: DatabaseHelper by injectLazy() - - val columnsPreference: SimpleDialogPreference by bindPref(R.string.pref_library_columns_dialog_key) - - val updateInterval: IntListPreference by bindPref(R.string.pref_library_update_interval_key) - - val updateRestriction: MultiSelectListPreference by bindPref(R.string.pref_library_update_restriction_key) - - val themePreference: IntListPreference by bindPref(R.string.pref_theme_key) - - val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key) - - val defaultCategory: IntListPreference by bindPref(R.string.default_category_key) - - val langPreference: ListPreference by bindPref(R.string.pref_language_key) - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - subscriptions += preferences.libraryUpdateInterval().asObservable() - .subscribe { updateRestriction.isVisible = it > 0 } - - subscriptions += Observable.combineLatest( - preferences.portraitColumns().asObservable(), - preferences.landscapeColumns().asObservable()) - { portraitColumns, landscapeColumns -> Pair(portraitColumns, landscapeColumns) } - .subscribe { updateColumnsSummary(it.first, it.second) } - - updateInterval.setOnPreferenceChangeListener { preference, newValue -> - // Always cancel the previous task, it seems that sometimes they are not updated. - LibraryUpdateJob.cancelTask() - - val interval = (newValue as String).toInt() - if (interval > 0) { - LibraryUpdateJob.setupTask(interval) - } - true - } - - updateRestriction.setOnPreferenceChangeListener { preference, newValue -> - // Post to event looper to allow the preference to be updated. - subscriptions += Observable.fromCallable { - LibraryUpdateJob.setupTask() - }.subscribeOn(AndroidSchedulers.mainThread()).subscribe() - - true - } - - val dbCategories = db.getCategories().executeAsBlocking() - categoryUpdate.apply { - entries = dbCategories.map { it.name }.toTypedArray() - entryValues = dbCategories.map { it.id.toString() }.toTypedArray() - } - - subscriptions += preferences.libraryUpdateCategories().asObservable() - .subscribe { - val selectedCategories = it - .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } } - .sortedBy { it.order } - - val summary = if (selectedCategories.isEmpty()) - getString(R.string.all) - else - selectedCategories.joinToString { it.name } - - categoryUpdate.summary = summary - } - - defaultCategory.apply { - val selectedCategory = dbCategories.find { it.id == preferences.defaultCategory()} - value = selectedCategory?.id?.toString() ?: value - entries += dbCategories.map { it.name }.toTypedArray() - entryValues += dbCategories.map { it.id.toString() }.toTypedArray() - summary = selectedCategory?.name ?: summary - } - - defaultCategory.setOnPreferenceChangeListener { _, newValue -> - defaultCategory.summary = dbCategories.find { - it.id == (newValue as String).toInt() - }?.name ?: getString(R.string.default_category_summary) - - true - } - - themePreference.setOnPreferenceChangeListener { preference, newValue -> - (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_THEME_CHANGED - activity.recreate() - true - } - - val langValues = langPreference.entryValues.map { value -> - val locale = LocaleHelper.getLocaleFromString(value.toString()) - locale?.getDisplayName(locale)?.capitalize() ?: context.getString(R.string.system_default) - } - - langPreference.entries = langValues.toTypedArray() - langPreference.setOnPreferenceChangeListener { preference, newValue -> - (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_LANG_CHANGED - LocaleHelper.changeLocale(newValue.toString()) - val app = activity.application - LocaleHelper.updateConfiguration(app, app.resources.configuration) - activity.recreate() - true - } - - } - - override fun onPreferenceDisplayDialog(p0: PreferenceFragmentCompat?, p: Preference): Boolean { - if (p === columnsPreference) { - val fragment = LibraryColumnsDialog.newInstance(p) - fragment.setTargetFragment(this, 0) - fragment.show(fragmentManager, null) - return true - } - return false - } - - private fun updateColumnsSummary(portraitColumns: Int, landscapeColumns: Int) { - val portrait = getColumnValue(portraitColumns) - val landscape = getColumnValue(landscapeColumns) - val msg = "${getString(R.string.portrait)}: $portrait, ${getString(R.string.landscape)}: $landscape" - - columnsPreference.summary = msg - } - - private fun getColumnValue(value: Int): String { - return if (value == 0) getString(R.string.default_columns) else value.toString() - } - -} 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 new file mode 100644 index 000000000..4953429fb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -0,0 +1,70 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.support.v7.preference.PreferenceScreen +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor + +class SettingsMainController : SettingsController() { + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.label_settings + + val tintColor = context.getResourceColor(R.attr.colorAccent) + + preference { + iconRes = R.drawable.ic_tune_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_general + onClick { navigateTo(SettingsGeneralController()) } + } + preference { + iconRes = R.drawable.ic_chrome_reader_mode_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_reader + onClick { navigateTo(SettingsReaderController()) } + } + preference { + iconRes = R.drawable.ic_file_download_black_24dp + iconTint = tintColor + 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 + titleRes = R.string.pref_category_tracking + onClick { navigateTo(SettingsTrackingController()) } + } + preference { + iconRes = R.drawable.ic_backup_black_24dp + iconTint = tintColor + titleRes = R.string.backup + onClick { navigateTo(SettingsBackupController()) } + } + preference { + iconRes = R.drawable.ic_code_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_advanced + onClick { navigateTo(SettingsAdvancedController()) } + } + preference { + iconRes = R.drawable.ic_help_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_about + onClick { navigateTo(SettingsAboutController()) } + } + } + + private fun navigateTo(controller: SettingsController) { + router.pushController(RouterTransaction.with(controller) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt new file mode 100644 index 000000000..03ec76a74 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -0,0 +1,111 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.support.v7.preference.PreferenceScreen +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsReaderController : SettingsController() { + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_reader + + intListPreference { + key = Keys.defaultViewer + titleRes = R.string.pref_viewer_type + entriesRes = arrayOf(R.string.left_to_right_viewer, R.string.right_to_left_viewer, + R.string.vertical_viewer, R.string.webtoon_viewer) + entryValues = arrayOf("1", "2", "3", "4") + defaultValue = "1" + summary = "%s" + } + intListPreference { + key = Keys.imageScaleType + titleRes = R.string.pref_image_scale_type + entriesRes = arrayOf(R.string.scale_type_fit_screen, R.string.scale_type_stretch, + R.string.scale_type_fit_width, R.string.scale_type_fit_height, + R.string.scale_type_original_size, R.string.scale_type_smart_fit) + entryValues = arrayOf("1", "2", "3", "4", "5", "6") + defaultValue = "1" + summary = "%s" + } + intListPreference { + key = Keys.zoomStart + titleRes = R.string.pref_zoom_start + entriesRes = arrayOf(R.string.zoom_start_automatic, R.string.zoom_start_left, + R.string.zoom_start_right, R.string.zoom_start_center) + entryValues = arrayOf("1", "2", "3", "4") + defaultValue = "1" + summary = "%s" + } + intListPreference { + key = Keys.rotation + titleRes = R.string.pref_rotation_type + entriesRes = arrayOf(R.string.rotation_free, R.string.rotation_lock, + R.string.rotation_force_portrait, R.string.rotation_force_landscape) + entryValues = arrayOf("1", "2", "3", "4") + defaultValue = "1" + summary = "%s" + } + intListPreference { + key = Keys.readerTheme + titleRes = R.string.pref_reader_theme + entriesRes = arrayOf(R.string.white_background, R.string.black_background) + entryValues = arrayOf("0", "1") + defaultValue = "0" + summary = "%s" + } + intListPreference { + key = Keys.imageDecoder + titleRes = R.string.pref_image_decoder + entries = arrayOf("Image", "Rapid", "Skia") + entryValues = arrayOf("0", "1", "2") + defaultValue = "0" + summary = "%s" + } + switchPreference { + key = Keys.fullscreen + titleRes = R.string.pref_fullscreen + defaultValue = true + } + switchPreference { + key = Keys.enableTransitions + titleRes = R.string.pref_page_transitions + defaultValue = true + } + switchPreference { + key = Keys.showPageNumber + titleRes = R.string.pref_show_page_number + defaultValue = true + } + switchPreference { + key = Keys.cropBorders + titleRes = R.string.pref_crop_borders + defaultValue = false + } + switchPreference { + key = Keys.keepScreenOn + titleRes = R.string.pref_keep_screen_on + defaultValue = true + } + preferenceCategory { + titleRes = R.string.pref_reader_navigation + + switchPreference { + key = Keys.readWithTapping + titleRes = R.string.pref_read_with_tapping + defaultValue = true + } + switchPreference { + key = Keys.readWithVolumeKeys + titleRes = R.string.pref_read_with_volume_keys + defaultValue = false + } + switchPreference { + key = Keys.readWithVolumeKeysInverted + titleRes = R.string.pref_read_with_volume_keys_inverted + defaultValue = false + }.apply { dependency = Keys.readWithVolumeKeys } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt similarity index 63% rename from app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt index 6f9349a5d..c7982d0d8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt @@ -1,47 +1,27 @@ package eu.kanade.tachiyomi.ui.setting -import android.content.Intent import android.graphics.drawable.Drawable -import android.os.Bundle -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import android.support.v7.preference.PreferenceGroup +import android.support.v7.preference.PreferenceScreen +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy import java.util.* -class SettingsSourcesFragment : SettingsFragment() { - - companion object { - const val SOURCE_CHANGE_REQUEST = 120 - - fun newInstance(rootKey: String?): SettingsSourcesFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsSourcesFragment().apply { arguments = args } - } - } - - private val preferences: PreferencesHelper by injectLazy() +class SettingsSourcesController : SettingsController(), + SourceLoginDialog.Listener { private val onlineSources by lazy { Injekt.get().getOnlineSources() } - override fun setDivider(divider: Drawable?) { - super.setDivider(null) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - // Remove dummy preference - preferenceScreen.removeAll() + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_sources // Get the list of active language codes. val activeLangsCodes = preferences.enabledLanguages().getOrDefault() @@ -66,8 +46,8 @@ class SettingsSourcesFragment : SettingsFragment() { addLanguageSources(this, sources) } - setOnPreferenceChangeListener { preference, any -> - val checked = any as Boolean + onChange { newValue -> + val checked = newValue as Boolean val current = preferences.enabledLanguages().getOrDefault() if (!checked) { preferences.enabledLanguages().set(current - lang) @@ -82,24 +62,28 @@ class SettingsSourcesFragment : SettingsFragment() { } } + override fun setDivider(divider: Drawable?) { + super.setDivider(null) + } + /** * Adds the source list for the given group (language). * * @param group the language category. */ - private fun addLanguageSources(group: SwitchPreferenceCategory, sources: List) { + private fun addLanguageSources(group: PreferenceGroup, sources: List) { val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault() sources.forEach { source -> - val sourcePreference = LoginCheckBoxPreference(context, source).apply { + val sourcePreference = LoginCheckBoxPreference(group.context, source).apply { val id = source.id.toString() title = source.name key = getSourceKey(source.id) isPersistent = false isChecked = id !in hiddenCatalogues - setOnPreferenceChangeListener { preference, any -> - val checked = any as Boolean + onChange { newValue -> + val checked = newValue as Boolean val current = preferences.hiddenCatalogues().getOrDefault() preferences.hiddenCatalogues().set(if (checked) @@ -111,27 +95,23 @@ class SettingsSourcesFragment : SettingsFragment() { } setOnLoginClickListener { - val fragment = SourceLoginDialog.newInstance(source) - fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST) - fragment.show(fragmentManager, null) + val dialog = SourceLoginDialog(source) + dialog.targetController = this@SettingsSourcesController + dialog.showDialog(router) } - } group.addPreference(sourcePreference) } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SOURCE_CHANGE_REQUEST && data != null) { - val sourceId = data.getLongExtra("key", -1L) - val pref = findPreference(getSourceKey(sourceId)) as? LoginCheckBoxPreference - pref?.notifyChanged() - } + override fun loginDialogClosed(source: LoginSource) { + val pref = findPreference(getSourceKey(source.id)) as? LoginCheckBoxPreference + pref?.notifyChanged() } private fun getSourceKey(sourceId: Long): String { return "source_$sourceId" } -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt new file mode 100644 index 000000000..0f7251bbf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -0,0 +1,91 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.app.Activity +import android.content.Intent +import android.support.customtabs.CustomTabsIntent +import android.support.v7.preference.PreferenceScreen +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.anilist.AnilistApi +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.widget.preference.LoginPreference +import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog +import uy.kohesive.injekt.injectLazy +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsTrackingController : SettingsController(), + TrackLoginDialog.Listener { + + private val trackManager: TrackManager by injectLazy() + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_tracking + + switchPreference { + key = Keys.autoUpdateTrack + titleRes = R.string.pref_auto_update_manga_sync + defaultValue = true + } + switchPreference { + key = Keys.askUpdateTrack + titleRes = R.string.pref_ask_update_manga_sync + defaultValue = false + }.apply { + dependency = Keys.autoUpdateTrack // the preference needs to be attached. + } + preferenceCategory { + titleRes = R.string.services + + trackPreference(trackManager.myAnimeList) { + onClick { + val dialog = TrackLoginDialog(trackManager.myAnimeList) + dialog.targetController = this@SettingsTrackingController + dialog.showDialog(router) + } + } + trackPreference(trackManager.aniList) { + onClick { + val tabsIntent = CustomTabsIntent.Builder() + .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) + .build() + tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + tabsIntent.launchUrl(activity, AnilistApi.authUrl()) + } + } + trackPreference(trackManager.kitsu) { + onClick { + val dialog = TrackLoginDialog(trackManager.kitsu) + dialog.targetController = this@SettingsTrackingController + dialog.showDialog(router) + } + } + } + } + + inline fun PreferenceScreen.trackPreference( + service: TrackService, + block: (@DSL LoginPreference).() -> Unit + ): LoginPreference { + return initThenAdd(LoginPreference(context).apply { + key = Keys.trackUsername(service.id) + title = service.name + }, block) + } + + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + // Manually refresh anilist holder + updatePreference(trackManager.aniList.id) + } + + private fun updatePreference(id: Int) { + val pref = findPreference(Keys.trackUsername(id)) as? LoginPreference + pref?.notifyChanged() + } + + override fun trackDialogClosed(service: TrackService) { + updatePreference(service.id) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt deleted file mode 100755 index 922d83958..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt +++ /dev/null @@ -1,95 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.content.Intent -import android.os.Bundle -import android.support.customtabs.CustomTabsIntent -import android.support.v7.preference.PreferenceCategory -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.data.track.anilist.AnilistApi -import eu.kanade.tachiyomi.util.getResourceColor -import eu.kanade.tachiyomi.widget.preference.LoginPreference -import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog -import uy.kohesive.injekt.injectLazy - -class SettingsTrackingFragment : SettingsFragment() { - - companion object { - const val SYNC_CHANGE_REQUEST = 121 - - fun newInstance(rootKey: String): SettingsTrackingFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsTrackingFragment().apply { arguments = args } - } - } - - private val trackManager: TrackManager by injectLazy() - - private val preferences: PreferencesHelper by injectLazy() - - val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_tracking_accounts_key) - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - registerService(trackManager.myAnimeList) - - registerService(trackManager.aniList) { - val intent = CustomTabsIntent.Builder() - .setToolbarColor(activity.getResourceColor(R.attr.colorPrimary)) - .build() - intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - intent.launchUrl(activity, AnilistApi.authUrl()) - } - - registerService(trackManager.kitsu) - } - - private fun registerService( - service: T, - onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) { - - LoginPreference(preferenceManager.context).apply { - key = preferences.keys.trackUsername(service.id) - title = service.name - - setOnPreferenceClickListener { - onPreferenceClick(service) - true - } - - syncCategory.addPreference(this) - } - } - - private val defaultOnPreferenceClick: (TrackService) -> Unit - get() = { - val fragment = TrackLoginDialog.newInstance(it) - fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST) - fragment.show(fragmentManager, null) - } - - override fun onResume() { - super.onResume() - // Manually refresh anilist holder - updatePreference(trackManager.aniList.id) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SYNC_CHANGE_REQUEST && data != null) { - val serviceId = data.getIntExtra("key", -1) - updatePreference(serviceId) - } - } - - private fun updatePreference(id: Int) { - val pref = findPreference(preferences.keys.trackUsername(id)) as? LoginPreference - pref?.notifyChanged() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/AndroidComponentUtil.java b/app/src/main/java/eu/kanade/tachiyomi/util/AndroidComponentUtil.java deleted file mode 100755 index 9b9402519..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/AndroidComponentUtil.java +++ /dev/null @@ -1,37 +0,0 @@ -package eu.kanade.tachiyomi.util; - -import android.app.ActivityManager; -import android.app.ActivityManager.RunningServiceInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; - -import timber.log.Timber; - -public final class AndroidComponentUtil { - - private AndroidComponentUtil() throws InstantiationException { - throw new InstantiationException("This class is not for instantiation"); - } - - public static void toggleComponent(Context context, Class componentClass, boolean enable) { - Timber.i((enable ? "Enabling " : "Disabling ") + componentClass.getSimpleName()); - ComponentName componentName = new ComponentName(context, componentClass); - PackageManager pm = context.getPackageManager(); - pm.setComponentEnabledSetting(componentName, - enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP); - } - - public static boolean isServiceRunning(Context context, Class serviceClass) { - ActivityManager manager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (serviceClass.getName().equals(service.service.getClassName())) { - return true; - } - } - return false; - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt index 16df65b59..9c5b49503 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt @@ -29,7 +29,7 @@ object ChapterRecognition { * Regex used to remove unwanted tags * Example Prison School 12 v.1 vol004 version1243 volume64 -R> Prison School 12 */ - private val unwanted = Regex("""(?:(v|ver|vol|version|volume|season|s).?[0-9]+)""") + private val unwanted = Regex("""(?): Boolean { + val className = serviceClass.name + val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + 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/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt index 1f1587fe6..ce231756c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt @@ -107,13 +107,20 @@ object DiskUtil { * Scans the given file so that it can be shown in gallery apps, for example. */ fun scanMedia(context: Context, file: File) { + scanMedia(context, Uri.fromFile(file)) + } + + /** + * Scans the given file so that it can be shown in gallery apps, for example. + */ + fun scanMedia(context: Context, uri: Uri) { val action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { Intent.ACTION_MEDIA_MOUNTED } else { Intent.ACTION_MEDIA_SCANNER_SCAN_FILE } val mediaScanIntent = Intent(action) - mediaScanIntent.data = Uri.fromFile(file) + mediaScanIntent.data = uri context.sendBroadcast(mediaScanIntent) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt index 7b5609f6f..30471b37c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt @@ -22,5 +22,5 @@ fun Element.attrOrText(css: String): String { * @param html the body of the response. Use only if the body was read before calling this method. */ fun Response.asJsoup(html: String? = null): Document { - return Jsoup.parse(html ?: body().string(), request().url().toString()) + return Jsoup.parse(html ?: body()!!.string(), request().url().toString()) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt index 79eb16ac9..823e1ce43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt @@ -24,7 +24,7 @@ class CustomLayoutFilePickerFragment : FilePickerFragment() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { when (viewType) { LogicHandler.VIEWTYPE_DIR -> { - val view = parent.inflate(R.layout.listitem_dir) + val view = parent.inflate(R.layout.common_listitem_dir) return DirViewHolder(view) } else -> return super.onCreateViewHolder(parent, viewType) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt index e84826004..327bb20aa 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt @@ -6,13 +6,13 @@ import android.util.AttributeSet import android.widget.LinearLayout import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.inflate -import kotlinx.android.synthetic.main.dialog_with_checkbox.view.* +import kotlinx.android.synthetic.main.common_dialog_with_checkbox.view.* class DialogCheckboxView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { init { - addView(inflate(R.layout.dialog_with_checkbox)) + addView(inflate(R.layout.common_dialog_with_checkbox)) } fun setDescription(@StringRes id: Int){ diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt new file mode 100644 index 000000000..078bceff4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.widget + +import android.support.v4.widget.DrawerLayout +import android.view.View +import android.view.ViewGroup + +class DrawerSwipeCloseListener( + private val drawer: DrawerLayout, + private val navigationView: ViewGroup +) : DrawerLayout.SimpleDrawerListener() { + + override fun onDrawerOpened(drawerView: View) { + if (drawerView == navigationView) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, drawerView) + } + } + + override fun onDrawerClosed(drawerView: View) { + if (drawerView == navigationView) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, drawerView) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt index 7950ff0f0..6715bb17d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt @@ -7,13 +7,13 @@ import android.widget.RelativeLayout import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.setVectorCompat -import kotlinx.android.synthetic.main.view_empty.view.* +import kotlinx.android.synthetic.main.common_view_empty.view.* class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RelativeLayout (context, attrs) { init { - inflate(context, R.layout.view_empty, this) + inflate(context, R.layout.common_view_empty, this) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt index 6d0044fe8..f644e3fdd 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.widget -import android.support.v4.view.PagerAdapter import android.view.View import android.view.ViewGroup +import com.nightlynexus.viewstatepageradapter.ViewStatePagerAdapter import java.util.* -abstract class RecyclerViewPagerAdapter : PagerAdapter() { +abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() { private val pool = Stack() @@ -21,22 +21,16 @@ abstract class RecyclerViewPagerAdapter : PagerAdapter() { protected open fun recycleView(view: View, position: Int) {} - override fun instantiateItem(container: ViewGroup, position: Int): Any { + override fun createView(container: ViewGroup, position: Int): View { val view = if (pool.isNotEmpty()) pool.pop() else createView(container) bindView(view, position) - container.addView(view) return view } - override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { - val view = obj as View + override fun destroyView(container: ViewGroup, position: Int, view: View) { recycleView(view, position) - container.removeView(view) if (recycle) pool.push(view) } - override fun isViewFromObject(view: View, obj: Any): Boolean { - return view === obj - } } \ 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 new file mode 100644 index 000000000..54cb768c3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java @@ -0,0 +1,281 @@ +/* + * 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/LibraryColumnsDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LibraryColumnsDialog.kt deleted file mode 100755 index 1577f424d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LibraryColumnsDialog.kt +++ /dev/null @@ -1,55 +0,0 @@ -package eu.kanade.tachiyomi.widget.preference - -import android.os.Bundle -import android.support.v7.preference.Preference -import android.support.v7.preference.PreferenceDialogFragmentCompat -import android.view.View -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import kotlinx.android.synthetic.main.pref_library_columns.view.* -import uy.kohesive.injekt.injectLazy - -class LibraryColumnsDialog : PreferenceDialogFragmentCompat() { - - companion object { - - fun newInstance(preference: Preference): LibraryColumnsDialog { - val fragment = LibraryColumnsDialog() - val bundle = Bundle(1) - bundle.putString("key", preference.key) - fragment.arguments = bundle - return fragment - } - } - - var portrait: Int = 0 - var landscape: Int = 0 - - val preferences: PreferencesHelper by injectLazy() - - override fun onBindDialogView(view: View) { - super.onBindDialogView(view) - - portrait = preferences.portraitColumns().getOrDefault() - landscape = preferences.landscapeColumns().getOrDefault() - - view.portrait_columns.value = portrait - view.landscape_columns.value = landscape - - view.portrait_columns.setOnValueChangedListener { picker, oldValue, newValue -> - portrait = newValue - } - - view.landscape_columns.setOnValueChangedListener { picker, oldValue, newValue -> - landscape = newValue - } - } - - override fun onDialogClosed(positiveResult: Boolean) { - if (positiveResult) { - preferences.portraitColumns().set(portrait) - preferences.landscapeColumns().set(landscape) - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt index a905b9f59..ac45bd4af 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.widget.preference import android.content.Context import android.graphics.Color +import android.support.v7.preference.CheckBoxPreference import android.support.v7.preference.PreferenceViewHolder import android.util.AttributeSet import android.view.View @@ -11,7 +12,6 @@ import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.setVectorCompat import kotlinx.android.synthetic.main.pref_item_source.view.* -import net.xpece.android.support.preference.CheckBoxPreference class LoginCheckBoxPreference @JvmOverloads constructor( context: Context, 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 a83d9712b..1b355d490 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 @@ -1,23 +1,22 @@ package eu.kanade.tachiyomi.widget.preference -import android.app.Activity import android.app.Dialog -import android.content.DialogInterface -import android.content.Intent import android.os.Bundle -import android.support.v4.app.DialogFragment import android.text.method.PasswordTransformationMethod import android.view.View import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType import com.dd.processbutton.iml.ActionProcessButton import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.widget.SimpleTextWatcher import kotlinx.android.synthetic.main.pref_account_login.view.* import rx.Subscription import uy.kohesive.injekt.injectLazy -abstract class LoginDialogPreference : DialogFragment() { +abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController(bundle) { var v: View? = null private set @@ -27,7 +26,7 @@ abstract class LoginDialogPreference : DialogFragment() { var requestSubscription: Subscription? = null override fun onCreateDialog(savedState: Bundle?): Dialog { - val dialog = MaterialDialog.Builder(activity) + val dialog = MaterialDialog.Builder(activity!!) .customView(R.layout.pref_account_login, false) .negativeText(android.R.string.cancel) .build() @@ -37,7 +36,7 @@ abstract class LoginDialogPreference : DialogFragment() { return dialog } - override fun onViewCreated(view: View, savedState: Bundle?) { + fun onViewCreated(view: View, savedState: Bundle?) { v = view.apply { show_password.setOnCheckedChangeListener { v, isChecked -> if (isChecked) @@ -55,7 +54,7 @@ abstract class LoginDialogPreference : DialogFragment() { password.addTextChangedListener(object : SimpleTextWatcher() { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (s.length == 0) { + if (s.isEmpty()) { show_password.isEnabled = true } } @@ -64,15 +63,15 @@ abstract class LoginDialogPreference : DialogFragment() { } - override fun onPause() { - super.onPause() - requestSubscription?.unsubscribe() + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (!type.isEnter) { + onDialogClosed() + } } - override fun onDismiss(dialog: DialogInterface) { - super.onDismiss(dialog) - val intent = Intent().putExtras(arguments) - targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, intent) + open fun onDialogClosed() { + requestSubscription?.unsubscribe() } protected abstract fun checkLogin() diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt index 5dbeb5db6..adc8f386a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginPreference.kt @@ -5,13 +5,13 @@ import android.support.v7.preference.Preference import android.support.v7.preference.PreferenceViewHolder import android.util.AttributeSet import eu.kanade.tachiyomi.R -import kotlinx.android.synthetic.main.preference_widget_imageview.view.* +import kotlinx.android.synthetic.main.pref_widget_imageview.view.* class LoginPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : Preference(context, attrs) { init { - widgetLayoutResource = R.layout.preference_widget_imageview + widgetLayoutResource = R.layout.pref_widget_imageview } override fun onBindViewHolder(holder: PreferenceViewHolder) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SimpleDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SimpleDialogPreference.kt deleted file mode 100755 index 7b31db1cb..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SimpleDialogPreference.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.tachiyomi.widget.preference - -import android.content.Context -import android.support.v7.preference.DialogPreference -import android.support.v7.preference.R.attr -import android.util.AttributeSet - -open class SimpleDialogPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = attr.dialogPreferenceStyle, defStyleRes: Int = 0) : - DialogPreference(context, attrs, defStyleAttr, defStyleRes) { - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt index b197ba6a8..42cf0b18e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt @@ -10,34 +10,17 @@ import eu.kanade.tachiyomi.util.toast import kotlinx.android.synthetic.main.pref_account_login.view.* import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -class SourceLoginDialog : LoginDialogPreference() { +class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) { - companion object { + private val source = Injekt.get().get(args.getLong("key")) as LoginSource - fun newInstance(source: Source): LoginDialogPreference { - val fragment = SourceLoginDialog() - val bundle = Bundle(1) - bundle.putLong("key", source.id) - fragment.arguments = bundle - return fragment - } - } - - val sourceManager: SourceManager by injectLazy() - - lateinit var source: LoginSource - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val sourceId = arguments.getLong("key") - source = sourceManager.get(sourceId) as LoginSource - } + constructor(source: Source) : this(Bundle().apply { putLong("key", source.id) }) override fun setCredentialsOnView(view: View) = with(view) { - dialog_title.text = getString(R.string.login_title, source.toString()) + dialog_title.text = context.getString(R.string.login_title, source.toString()) username.setText(preferences.sourceUsername(source)) password.setText(preferences.sourcePassword(source)) } @@ -60,7 +43,7 @@ class SourceLoginDialog : LoginDialogPreference() { username.text.toString(), password.text.toString()) - dialog.dismiss() + dialog?.dismiss() context.toast(R.string.login_success) } else { preferences.setSourceCredentials(source, "", "") @@ -74,4 +57,13 @@ class SourceLoginDialog : LoginDialogPreference() { } } + override fun onDialogClosed() { + super.onDialogClosed() + (targetController as? Listener)?.loginDialogClosed(source) + } + + interface Listener { + fun loginDialogClosed(source: LoginSource) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt index f07ccbc72..cff0847c1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SwitchPreferenceCategory.kt @@ -4,15 +4,16 @@ import android.annotation.TargetApi import android.content.Context import android.content.res.TypedArray import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH +import android.support.v7.preference.PreferenceCategory import android.support.v7.preference.PreferenceViewHolder import android.support.v7.widget.SwitchCompat import android.util.AttributeSet import android.view.View import android.widget.Checkable import android.widget.CompoundButton +import android.widget.TextView +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.getResourceColor -import net.xpece.android.support.preference.PreferenceCategory -import net.xpece.android.support.preference.R class SwitchPreferenceCategory @JvmOverloads constructor( context: Context, @@ -20,20 +21,17 @@ class SwitchPreferenceCategory @JvmOverloads constructor( : PreferenceCategory( context, attrs, - R.attr.switchPreferenceCompatStyle, - R.style.Preference_Material_SwitchPreferenceCompat), + R.attr.switchPreferenceCompatStyle), CompoundButton.OnCheckedChangeListener { - init { - setTitleTextColor(context.getResourceColor(R.attr.colorAccent)) - } - private var mChecked = false private var mCheckedSet = false override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) + val titleView = holder.findViewById(android.R.id.title) as TextView + titleView.setTextColor(context.getResourceColor(R.attr.colorAccent)) syncSwitchView(holder) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt index 58da50ff2..f9fc355c0 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt @@ -9,36 +9,19 @@ import eu.kanade.tachiyomi.util.toast import kotlinx.android.synthetic.main.pref_account_login.view.* import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -class TrackLoginDialog : LoginDialogPreference() { +class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) { - companion object { + private val service = Injekt.get().getService(args.getInt("key"))!! - fun newInstance(sync: TrackService): LoginDialogPreference { - val fragment = TrackLoginDialog() - val bundle = Bundle(1) - bundle.putInt("key", sync.id) - fragment.arguments = bundle - return fragment - } - } - - val trackManager: TrackManager by injectLazy() - - lateinit var sync: TrackService - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val syncId = arguments.getInt("key") - sync = trackManager.getService(syncId)!! - } + constructor(service: TrackService) : this(Bundle().apply { putInt("key", service.id) }) override fun setCredentialsOnView(view: View) = with(view) { - dialog_title.text = getString(R.string.login_title, sync.name) - username.setText(sync.getUsername()) - password.setText(sync.getPassword()) + dialog_title.text = context.getString(R.string.login_title, service.name) + username.setText(service.getUsername()) + password.setText(service.getPassword()) } override fun checkLogin() { @@ -52,11 +35,11 @@ class TrackLoginDialog : LoginDialogPreference() { val user = username.text.toString() val pass = password.text.toString() - requestSubscription = sync.login(user, pass) + requestSubscription = service.login(user, pass) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ - dialog.dismiss() + dialog?.dismiss() context.toast(R.string.login_success) }, { error -> login.progress = -1 @@ -67,4 +50,13 @@ class TrackLoginDialog : LoginDialogPreference() { } } + override fun onDialogClosed() { + super.onDialogClosed() + (targetController as? Listener)?.trackDialogClosed(service) + } + + interface Listener { + fun trackDialogClosed(service: TrackService) + } + } diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddFragment.kt b/app/src/main/java/exh/ui/batchadd/BatchAddController.kt similarity index 100% rename from app/src/main/java/exh/ui/batchadd/BatchAddFragment.kt rename to app/src/main/java/exh/ui/batchadd/BatchAddController.kt diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt b/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt new file mode 100644 index 000000000..439487748 --- /dev/null +++ b/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt @@ -0,0 +1,5 @@ +package exh.ui.batchadd + +/** + * Created by nulldev on 8/23/17. + */ 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 new file mode 100644 index 000000000..4ad2729af --- /dev/null +++ b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..96494d93a --- /dev/null +++ b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector_amoled.xml b/app/src/main/res/drawable/library_item_selector_amoled.xml new file mode 100644 index 000000000..92cb0db94 --- /dev/null +++ b/app/src/main/res/drawable/library_item_selector_amoled.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/list_item_selector_amoled.xml b/app/src/main/res/drawable/list_item_selector_amoled.xml new file mode 100644 index 000000000..e573c82bb --- /dev/null +++ b/app/src/main/res/drawable/list_item_selector_amoled.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/manga_info_controller.xml b/app/src/main/res/layout-land/manga_info_controller.xml new file mode 100644 index 000000000..fa49622b5 --- /dev/null +++ b/app/src/main/res/layout-land/manga_info_controller.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_download_manager.xml b/app/src/main/res/layout/activity_download_manager.xml deleted file mode 100755 index c99c4f698..000000000 --- a/app/src/main/res/layout/activity_download_manager.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_edit_categories.xml b/app/src/main/res/layout/activity_edit_categories.xml deleted file mode 100755 index 0668bb24e..000000000 --- a/app/src/main/res/layout/activity_edit_categories.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_manga.xml b/app/src/main/res/layout/activity_manga.xml deleted file mode 100755 index e929653a9..000000000 --- a/app/src/main/res/layout/activity_manga.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_preferences.xml b/app/src/main/res/layout/activity_preferences.xml deleted file mode 100755 index 7c1371b4f..000000000 --- a/app/src/main/res/layout/activity_preferences.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_catalogue.xml b/app/src/main/res/layout/catalogue_controller.xml similarity index 98% rename from app/src/main/res/layout/fragment_catalogue.xml rename to app/src/main/res/layout/catalogue_controller.xml index 92d1845d1..48d8601ba 100755 --- a/app/src/main/res/layout/fragment_catalogue.xml +++ b/app/src/main/res/layout/catalogue_controller.xml @@ -11,7 +11,7 @@ android:fitsSystemWindows="true" android:orientation="vertical" android:id="@+id/catalogue_view" - tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment"> + tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"> - \ No newline at end of file + tools:listitem="@layout/catalogue_grid_item" /> \ No newline at end of file diff --git a/app/src/main/res/layout/categories_controller.xml b/app/src/main/res/layout/categories_controller.xml new file mode 100644 index 000000000..f0930b6ce --- /dev/null +++ b/app/src/main/res/layout/categories_controller.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_edit_categories.xml b/app/src/main/res/layout/categories_item.xml similarity index 100% rename from app/src/main/res/layout/item_edit_categories.xml rename to app/src/main/res/layout/categories_item.xml diff --git a/app/src/main/res/layout/fragment_manga_chapters.xml b/app/src/main/res/layout/chapters_controller.xml similarity index 97% rename from app/src/main/res/layout/fragment_manga_chapters.xml rename to app/src/main/res/layout/chapters_controller.xml index 11412d180..7c2b8f2ec 100755 --- a/app/src/main/res/layout/fragment_manga_chapters.xml +++ b/app/src/main/res/layout/chapters_controller.xml @@ -29,7 +29,7 @@ android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:descendantFocusability="blocksDescendants" - tools:listitem="@layout/item_chapter"> + tools:listitem="@layout/chapters_item"> diff --git a/app/src/main/res/layout/chapters_item.xml b/app/src/main/res/layout/chapters_item.xml new file mode 100644 index 000000000..1b04056a8 --- /dev/null +++ b/app/src/main/res/layout/chapters_item.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_with_checkbox.xml b/app/src/main/res/layout/common_dialog_with_checkbox.xml similarity index 100% rename from app/src/main/res/layout/dialog_with_checkbox.xml rename to app/src/main/res/layout/common_dialog_with_checkbox.xml diff --git a/app/src/main/res/layout/listitem_dir.xml b/app/src/main/res/layout/common_listitem_dir.xml similarity index 100% rename from app/src/main/res/layout/listitem_dir.xml rename to app/src/main/res/layout/common_listitem_dir.xml diff --git a/app/src/main/res/layout/spinner_item.xml b/app/src/main/res/layout/common_spinner_item.xml similarity index 100% rename from app/src/main/res/layout/spinner_item.xml rename to app/src/main/res/layout/common_spinner_item.xml diff --git a/app/src/main/res/layout/view_empty.xml b/app/src/main/res/layout/common_view_empty.xml similarity index 100% rename from app/src/main/res/layout/view_empty.xml rename to app/src/main/res/layout/common_view_empty.xml diff --git a/app/src/main/res/layout/download_controller.xml b/app/src/main/res/layout/download_controller.xml new file mode 100644 index 000000000..8b353b1fe --- /dev/null +++ b/app/src/main/res/layout/download_controller.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/download_item.xml similarity index 100% rename from app/src/main/res/layout/item_download.xml rename to app/src/main/res/layout/download_item.xml diff --git a/app/src/main/res/layout/eh_activity_finish_migration.xml b/app/src/main/res/layout/eh_activity_finish_migration.xml deleted file mode 100755 index e96f46e38..000000000 --- a/app/src/main/res/layout/eh_activity_finish_migration.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_backup.xml b/app/src/main/res/layout/fragment_backup.xml deleted file mode 100755 index e13611cf7..000000000 --- a/app/src/main/res/layout/fragment_backup.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - -