diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 3cc67de17..a708214a9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -67,6 +67,8 @@ class AppModule(val app: Application) : InjektModule { GlobalScope.launch { get() } + // SY --> GlobalScope.launch { get() } + // SY <-- } } 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 76475581d..de92127fb 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 @@ -105,6 +105,10 @@ class BackupRestoreService : Service() { // SY --> private val throttleManager = EHentaiThrottleManager() + + private var skippedAmount = 0 + + private var totalAmount = 0 // SY <-- /** @@ -117,12 +121,6 @@ class BackupRestoreService : Service() { */ private var restoreAmount = 0 - // SY --> - private var skippedAmount = 0 - - private var totalAmount = 0 - // SY <-- - /** * Mapping of source ID to source name from backup data */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt index eae1de21f..c6ad99d34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt @@ -14,7 +14,9 @@ object MangaTypeAdapter { write { beginArray() value(it.url) + // SY --> value(it.originalTitle) + // SY <-- value(it.source) value(it.viewer) value(it.chapter_flags) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index 98be1de12..8c433d36a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt @@ -52,11 +52,13 @@ class MangaPutResolver : DefaultPutResolver() { put(COL_ID, obj.id) put(COL_SOURCE, obj.source) put(COL_URL, obj.url) + // SY --> put(COL_ARTIST, obj.originalArtist) put(COL_AUTHOR, obj.originalAuthor) put(COL_DESCRIPTION, obj.originalDescription) put(COL_GENRE, obj.originalGenre) put(COL_TITLE, obj.originalTitle) + // SY <-- put(COL_STATUS, obj.status) put(COL_THUMBNAIL_URL, obj.thumbnail_url) put(COL_FAVORITE, obj.favorite) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index f8502d117..70cd97f43 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -11,6 +11,7 @@ open class MangaImpl : Manga { override lateinit var url: String + // SY --> private val customMangaManager: CustomMangaManager by injectLazy() override var title: String @@ -39,6 +40,7 @@ open class MangaImpl : Manga { override var genre: String? get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre set(value) { ogGenre = value } + // SY <-- override var status: Int = 0 @@ -58,6 +60,7 @@ open class MangaImpl : Manga { override var cover_last_modified: Long = 0 + // SY --> lateinit var ogTitle: String private set var ogAuthor: String? = null @@ -68,6 +71,7 @@ open class MangaImpl : Manga { private set var ogGenre: String? = null private set + // SY <-- override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 047014237..a96c44805 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -22,7 +22,7 @@ import uy.kohesive.injekt.injectLazy * * @param context the application context. */ -class DownloadManager(val context: Context) { +class DownloadManager(/* SY private */ val context: Context) { /** * The sources manager. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 40ace2ad3..ba8546487 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -118,7 +118,9 @@ class DownloadProvider(private val context: Context) { * @param manga the manga to query. */ fun getMangaDirName(manga: Manga): String { + // SY --> return DiskUtil.buildValidFilename(manga.originalTitle) + // SY <-- } /** 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 36d058e52..8940d068b 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 @@ -67,6 +67,8 @@ object PreferenceKeys { const val landscapeColumns = "pref_library_columns_landscape_key" + const val jumpToChapters = "jump_to_chapters" + const val updateOnlyNonCompleted = "pref_update_only_non_completed_key" const val autoUpdateTrack = "pref_auto_update_manga_sync_key" @@ -243,8 +245,6 @@ object PreferenceKeys { const val eh_is_hentai_enabled = "eh_is_hentai_enabled" - const val eh_use_new_manga_interface = "eh_use_new_manga_interface" - const val eh_use_auto_webtoon = "eh_use_auto_webtoon" const val eh_watched_list_default_state = "eh_watched_list_default_state" 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 68d9bcb23..f43c6fb1c 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 @@ -131,6 +131,8 @@ class PreferencesHelper(val context: Context) { fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0) + fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false) + fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false) fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) @@ -347,8 +349,6 @@ class PreferencesHelper(val context: Context) { fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4) - fun eh_useNewMangaInterface() = flowPrefs.getBoolean(Keys.eh_use_new_manga_interface, true) - fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true) fun eh_watchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false) 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 ac8c2d7e6..a17d65d99 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -136,6 +136,7 @@ class LocalSource(private val context: Context) : CatalogueSource { return Observable.just(MangasPage(mangas, false)) } + // SY --> fun updateMangaInfo(manga: SManga) { val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find { it.exists() @@ -173,6 +174,7 @@ class LocalSource(private val context: Context) : CatalogueSource { return title.hashCode() } } + // SY <-- override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt index 889666180..e327ebc36 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt @@ -29,7 +29,9 @@ sealed class Filter(val name: String, var state: T) { data class Selection(val index: Int, val ascending: Boolean) } + // SY --> abstract class AutoComplete(name: String, val hint: String, val values: List, val skipAutoFillTags: List = emptyList(), val excludePrefix: String? = null, state: List) : Filter>(name, state) + // SY <-- override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index cf475ebb1..a2dff45af 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -23,6 +23,7 @@ interface SManga : Serializable { var initialized: Boolean + // SY --> val originalTitle: String get() = (this as? MangaImpl)?.ogTitle ?: title val originalAuthor: String? @@ -33,6 +34,7 @@ interface SManga : Serializable { get() = (this as? MangaImpl)?.ogDesc ?: description val originalGenre: String? get() = (this as? MangaImpl)?.ogGenre ?: genre + // SY <-- fun copyFrom(other: SManga) { // EXH --> @@ -42,19 +44,19 @@ interface SManga : Serializable { // EXH <-- if (other.author != null) { - author = other.originalAuthor + author = /* SY --> */ other.originalAuthor /* SY <-- */ } if (other.artist != null) { - artist = other.originalArtist + artist = /* SY --> */ other.originalArtist /* SY <-- */ } if (other.description != null) { - description = other.originalDescription + description = /* SY --> */ other.originalDescription /* SY <-- */ } if (other.genre != null) { - genre = other.originalGenre + genre = /* SY --> */ other.originalGenre /* SY <-- */ } if (other.thumbnail_url != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt index 5726441d0..b314f6c1a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt @@ -48,7 +48,9 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) : extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase() extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase() extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase() + // SY --> extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase() + // SY <-- else -> null } @@ -91,12 +93,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) : setText(R.string.ext_update) } else -> { + // SY --> if (extension.sources.any { it is ConfigurableSource }) { @SuppressLint("SetTextI18n") text = context.getString(R.string.action_settings) + "+" } else { setText(R.string.action_settings) } + // SY <-- } } } else if (extension is Extension.Untrusted) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index fefc3717c..a132bf30d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension.details import android.annotation.SuppressLint import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.provider.Settings import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu @@ -180,6 +183,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : when (item.itemId) { R.id.action_enable_all -> toggleAllSources(true) R.id.action_disable_all -> toggleAllSources(false) + R.id.action_open_in_settings -> openInSettings() } return super.onOptionsItemSelected(item) } @@ -204,6 +208,13 @@ class ExtensionDetailsController(bundle: Bundle? = null) : ) } + private fun openInSettings() { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", presenter.pkgName, null) + } + startActivity(intent) + } + private fun Source.isEnabled(): Boolean { return id.toString() !in preferences.disabledSources().get() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt index dccda3f5f..d59e6d0a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/latest/LatestController.kt @@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import kotlinx.coroutines.flow.launchIn @@ -72,11 +71,7 @@ open class LatestController : */ override fun onMangaClick(manga: Manga) { // Open MangaController. - if (presenter.preferences.eh_useNewMangaInterface().get()) { - parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction()) - } else { - parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction()) - } + parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt index 2d17ba08d..ecc50d6df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt @@ -29,7 +29,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.lang.launchUI @@ -427,7 +426,7 @@ class MigrationListController(bundle: Bundle? = null) : private fun navigateOut() { if (migratingManga?.size == 1) { launchUI { - val hasDetails = router.backstack.any { it.controller() is MangaController } || router.backstack.any { it.controller() is MangaAllInOneController } + val hasDetails = router.backstack.any { it.controller() is MangaController } if (hasDetails) { val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let { db.getManga(it).executeOnIO() @@ -435,10 +434,9 @@ class MigrationListController(bundle: Bundle? = null) : if (manga != null) { val newStack = router.backstack.filter { it.controller() !is MangaController && - it.controller() !is MangaAllInOneController && it.controller() !is MigrationListController && it.controller() !is PreMigrationController - } + if (preferences.eh_useNewMangaInterface().get()) MangaAllInOneController(manga).withFadeTransaction() else MangaController(manga).withFadeTransaction() + } + MangaController(manga).withFadeTransaction() router.setBackstack(newStack, FadeChangeHandler()) return@launchUI } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt index d1d5429c2..c134cc2b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt @@ -9,13 +9,11 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.toMangaThumbnail -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.getResourceColor @@ -38,7 +36,6 @@ import kotlinx.android.synthetic.main.migration_process_item.migration_menu import kotlinx.android.synthetic.main.migration_process_item.skip_manga import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -85,21 +82,12 @@ class MigrationProcessHolder( withContext(Dispatchers.Main) { migration_manga_card_from.attachManga(manga, source) migration_manga_card_from.setOnClickListener { - if (Injekt.get().eh_useNewMangaInterface().get()) { - adapter.controller.router.pushController( - MangaAllInOneController( - manga, - true - ).withFadeTransaction() - ) - } else { - adapter.controller.router.pushController( - MangaController( - manga, - true - ).withFadeTransaction() - ) - } + adapter.controller.router.pushController( + MangaController( + manga, + true + ).withFadeTransaction() + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index bfb66bfa8..e4396b065 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -54,7 +54,7 @@ class SourceController(bundle: Bundle? = null) : FlexibleAdapter.OnItemLongClickListener, SourceAdapter.OnBrowseClickListener, SourceAdapter.OnLatestClickListener, - ChangeSourceCategoriesDialog.Listener { + /*SY -->*/ ChangeSourceCategoriesDialog.Listener /*SY <--*/ { private val preferences: PreferencesHelper = Injekt.get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index eb94a1b9e..15a9bdfb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -42,7 +42,6 @@ import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesControll import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet.FilterNavigationView.Companion.MAX_SAVED_SEARCHES import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.system.connectivityManager @@ -304,7 +303,6 @@ open class BrowseSourceController(bundle: Bundle) : } // EXH <-- ) - filterSheet?.setFilters(presenter.filterItems) // TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly @@ -713,23 +711,13 @@ open class BrowseSourceController(bundle: Bundle) : // SY --> when (mode) { Mode.CATALOGUE -> { - if (preferences.eh_useNewMangaInterface().get()) { - router.pushController( - MangaAllInOneController( - item.manga, - true, - args.getParcelable(SMART_SEARCH_CONFIG_KEY) - ).withFadeTransaction() - ) - } else { - router.pushController( - MangaController( - item.manga, - true, - args.getParcelable(SMART_SEARCH_CONFIG_KEY) - ).withFadeTransaction() - ) - } + router.pushController( + MangaController( + item.manga, + true, + args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA) + ).withFadeTransaction() + ) } Mode.RECOMMENDS -> openSmartSearch(item.manga.originalTitle) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt index 72b9d2af7..8df3a899b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt @@ -35,14 +35,16 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) : * @param manga the manga to bind. */ override fun onSetValues(manga: Manga) { + title.text = manga.title title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor) + // Set alpha of thumbnail. + thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f + setImage(manga) } override fun setImage(manga: Manga) { - title.text = manga.title - GlideApp.with(view.context).clear(thumbnail) if (!manga.thumbnail_url.isNullOrEmpty()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt index 8061ad09c..021427ea1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt @@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn @@ -76,14 +75,7 @@ open class GlobalSearchController( * @param manga clicked item containing manga information. */ override fun onMangaClick(manga: Manga) { - // Open MangaController. - // SY --> - if (presenter.preferences.eh_useNewMangaInterface().get()) { - router.pushController(MangaAllInOneController(manga, true).withFadeTransaction()) - } else { - router.pushController(MangaController(manga, true).withFadeTransaction()) - } - // SY <-- + router.pushController(MangaController(manga, true).withFadeTransaction()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index e1206f6ce..918243231 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesControlle import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.toast @@ -572,13 +571,7 @@ class LibraryController( // Notify the presenter a manga is being opened. presenter.onOpenManga() - // SY --> - if (preferences.eh_useNewMangaInterface().get()) { - router.pushController(MangaAllInOneController(manga).withFadeTransaction()) - } else { - router.pushController(MangaController(manga).withFadeTransaction()) - } - // SY <-- + router.pushController(MangaController(manga).withFadeTransaction()) } /** 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 06515e3ab..2caf50c8b 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 @@ -36,7 +36,6 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.more.MoreController import eu.kanade.tachiyomi.ui.recent.history.HistoryController @@ -309,13 +308,7 @@ class MainActivity : BaseActivity() { router.popToRoot() } setSelectedNavItem(R.id.nav_library) - // SY --> - if (preferences.eh_useNewMangaInterface().get()) { - router.pushController(RouterTransaction.with(MangaAllInOneController(extras))) - } else { - router.pushController(RouterTransaction.with(MangaController(extras))) - } - // SY <-- + router.pushController(RouterTransaction.with(MangaController(extras))) } SHORTCUT_DOWNLOADS -> { if (router.backstackSize > 1) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 519a3c3d8..0d52192cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -48,9 +48,9 @@ class EditMangaDialog : DialogController { private var willResetCover = false private val infoController - get() = targetController as MangaAllInOneController + get() = targetController as MangaController - constructor(target: MangaAllInOneController, manga: Manga) : super( + constructor(target: MangaController, manga: Manga) : super( Bundle() .apply { putLong(KEY_MANGA, manga.id!!) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt deleted file mode 100644 index 5b5b6009b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt +++ /dev/null @@ -1,82 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.content.Context -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.Source -import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterItem -import eu.kanade.tachiyomi.util.system.getResourceColor -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.util.Date -import kotlinx.coroutines.CoroutineScope -import uy.kohesive.injekt.injectLazy - -class MangaAllInOneAdapter( - controller: MangaAllInOneController, - context: Context -) : FlexibleAdapter>(null, controller, true) { - - val delegate: MangaAllInOneInterface = controller - - val preferences: PreferencesHelper by injectLazy() - - var items: List = emptyList() - - val readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f) - val unreadColor = context.getResourceColor(R.attr.colorOnSurface) - - val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) - - val decimalFormat = DecimalFormat( - "#.###", - DecimalFormatSymbols() - .apply { decimalSeparator = '.' } - ) - - val dateFormat: DateFormat = preferences.dateFormat() - - override fun updateDataSet(items: List>?) { - this.items = items as List? ?: emptyList() - super.updateDataSet(items) - } - - fun indexOf(item: MangaAllInOneChapterItem): Int { - return items.indexOf(item) - } - - interface MangaAllInOneInterface : MangaHeaderInterface - - interface MangaHeaderInterface { - fun openSmartSearch() - fun mangaPresenter(): MangaAllInOnePresenter - fun openRecommends() - fun onNextManga(manga: Manga, source: Source, chapters: List, lastUpdateDate: Date, chapterCount: Float) - fun setMangaInfo(manga: Manga, source: Source?, chapters: List, lastUpdateDate: Date, chapterCount: Float) - fun openInWebView() - fun shareManga() - fun fetchMangaFromSource(manualFetch: Boolean = false, fetchManga: Boolean = true, fetchChapters: Boolean = true) - fun onFetchMangaDone() - fun onFetchMangaError(error: Throwable) - fun setRefreshing(value: Boolean) - fun onFavoriteClick() - fun onCategoriesClick() - fun updateCategoriesForMangas(mangas: List, categories: List) - fun performGlobalSearch(query: String) - fun wrapTag(namespace: String, tag: String): String - fun isEHentaiBasedSource(): Boolean - fun performSearch(query: String) - fun openTracking() - suspend fun mergeWithAnother() - fun copyToClipboard(label: String, text: String) - fun migrateManga() - fun isInitialLoadAndFromSource(): Boolean - fun removeInitialLoad() - val controllerScope: CoroutineScope - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt deleted file mode 100644 index dc00fdf31..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt +++ /dev/null @@ -1,1069 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.core.graphics.drawable.DrawableCompat -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.MangaAllInOneControllerBinding -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController -import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter -import eu.kanade.tachiyomi.ui.manga.chapter.DeleteChaptersDialog -import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog -import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterHolder -import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterItem -import eu.kanade.tachiyomi.ui.manga.track.TrackController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.ui.recent.history.HistoryController -import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController -import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.getCoordinates -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.shrinkOnScroll -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.visible -import exh.EH_SOURCE_ID -import exh.EXH_SOURCE_ID -import java.io.IOException -import java.util.Date -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.swiperefreshlayout.refreshes -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy - -/** - * Fragment that shows manga information. - * Uses R.layout.manga_info_controller. - * UI related actions should be called from here. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class MangaAllInOneController : - NucleusController, - ChangeMangaCategoriesDialog.Listener, - CoroutineScope, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - DownloadCustomChaptersDialog.Listener, - DeleteChaptersDialog.Listener, - MangaAllInOneAdapter.MangaAllInOneInterface { - - constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: Boolean = false) : super( - Bundle().apply { - putLong(MANGA_EXTRA, manga?.id ?: 0) - putBoolean(FROM_SOURCE_EXTRA, fromSource) - putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) - putBoolean(UPDATE_EXTRA, update) - } - ) { - this.manga = manga - if (manga != null) { - source = Injekt.get().getOrStub(manga.source) - } - } - - // EXH --> - constructor(redirect: ChaptersPresenter.EXHRedirect) : super( - Bundle().apply { - putLong(MANGA_EXTRA, redirect.manga.id!!) - putBoolean(UPDATE_EXTRA, redirect.update) - } - ) { - this.manga = redirect.manga - if (manga != null) { - source = Injekt.get().getOrStub(redirect.manga.source) - } - } - // EXH <-- - - 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 val preferences: PreferencesHelper by injectLazy() - - /** - * Adapter containing a list of chapters. - */ - private var adapter: MangaAllInOneAdapter? = null - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - /** - * Selected items. Used to restore selections after a rotation. - */ - private val selectedItems = mutableSetOf() - - private var lastClickPosition = -1 - - private var initialLoad: Boolean = true - - // EXH --> - val smartSearchConfig: SourceController.SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG_EXTRA) - - override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main - - private var editMangaDialog: EditMangaDialog? = null - // EXH <-- - - val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) - - var update = args.getBoolean(UPDATE_EXTRA, false) - - override val controllerScope = scope - - init { - setHasOptionsMenu(true) - } - - override fun getTitle(): String? { - return manga?.title - } - - override fun createPresenter(): MangaAllInOnePresenter { - return MangaAllInOnePresenter( - this, manga!!, source!!, smartSearchConfig - ) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = MangaAllInOneControllerBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - if (manga == null || source == null) return - - // Set SwipeRefresh to refresh manga data. - binding.swipeRefresh.refreshes() - .onEach { fetchMangaFromSource(manualFetch = true) } - .launchIn(scope) - - // Init RecyclerView and adapter - adapter = MangaAllInOneAdapter(this, view.context) - - binding.recycler.adapter = adapter - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - binding.recycler.setHasFixedSize(true) - adapter?.fastScroller = binding.fastScroller - - binding.fab.clicks() - .onEach { - 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 = binding.fab.getCoordinates() - if (!binding.revealView.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else { - view.context.toast(R.string.no_next_chapter) - } - } - .launchIn(scope) - - binding.fab.shrinkOnScroll(binding.recycler) - - binding.actionToolbar.offsetAppbarHeight(activity!!) - binding.fab.offsetAppbarHeight(activity!!) - } - - private fun getHeader(): MangaAllInOneHolder? { - return binding.recycler.findViewHolderForAdapterPosition(0) as? MangaAllInOneHolder - } - - private fun addMangaHeader() { - if (adapter?.scrollableHeaders?.isEmpty() == true) { - adapter?.removeAllScrollableHeaders() - adapter?.addScrollableHeader(presenter.headerItem) - } - } - - // EXH --> - override fun openSmartSearch() { - val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.title, presenter.manga.id) - - router?.pushController( - SourceController( - Bundle().apply { - putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig) - } - ).withFadeTransaction() - ) - } - - override suspend fun mergeWithAnother() { - try { - val mergedManga = withContext(Dispatchers.IO + NonCancellable) { - presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!) - } - - router?.pushController( - MangaAllInOneController( - mergedManga, - true, - update = true - ).withFadeTransaction() - ) - applicationContext?.toast("Manga merged!") - } catch (e: Exception) { - if (e is CancellationException) throw e - else { - applicationContext?.toast("Failed to merge manga: ${e.message}") - } - } - } - - override fun copyToClipboard(label: String, text: String) { - activity!!.copyToClipboard(label, text) - } - - override fun migrateManga() { - PreMigrationController.navigateToMigration( - preferences.skipPreMigration().get(), - router, - listOf(presenter.manga.id!!) - ) - } - - override fun mangaPresenter(): MangaAllInOnePresenter { - return presenter - } - // EXH <-- - - // AZ --> - override fun openRecommends() { - val recommendsConfig = BrowseSourceController.RecommendsConfig(presenter.manga) - - router?.pushController( - BrowseSourceController( - Bundle().apply { - putParcelable(BrowseSourceController.RECOMMENDS_CONFIG, recommendsConfig) - } - ).withFadeTransaction() - ) - } - // AZ <-- - - override fun openTracking() { - router?.pushController( - TrackController(fromAllInOne = true, manga = manga).withFadeTransaction() - ) - } - - /** - * 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. - */ - override fun onNextManga(manga: Manga, source: Source, chapters: List, lastUpdateDate: Date, chapterCount: Float) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source, chapters, lastUpdateDate, chapterCount) - if (!presenter.hasRequested && presenter.chapters.isEmpty()) { - fetchMangaFromSource(fetchManga = false) - } - } 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. - */ - override fun setMangaInfo(manga: Manga, source: Source?, chapters: List, lastUpdateDate: Date, chapterCount: Float) { - val view = view ?: return - - if (update || - // Auto-update old format galleries - ( - (presenter.manga.source == EH_SOURCE_ID || presenter.manga.source == EXH_SOURCE_ID) && - chapters.size == 1 && chapters.first().date_upload == 0L - ) - ) { - update = false - fetchMangaFromSource() - } - - val adapter = adapter ?: return - adapter.updateDataSet(chapters) - addMangaHeader() - adapter.recyclerView?.post { - setChapterCount(chapterCount) - setLastUpdateDate(lastUpdateDate) - } - - 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() - } - - if (view.context != null && chapters.any { it.read }) { - binding.fab.text = view.context.getString(R.string.action_resume) - } - } - - override fun openInWebView() { - val source = presenter.source as? HttpSource ?: return - - val url = try { - source.mangaDetailsRequest(presenter.manga).url.toString() - } catch (e: Exception) { - return - } - - val activity = activity ?: return - val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title) - startActivity(intent) - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - override fun shareManga() { - val context = view?.context ?: return - - val source = presenter.source as? HttpSource ?: return - try { - val url = source.mangaDetailsRequest(presenter.manga).url.toString() - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Start fetching manga information from source. - */ - override fun fetchMangaFromSource(manualFetch: Boolean, fetchManga: Boolean, fetchChapters: Boolean) { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource(manualFetch, fetchManga, fetchChapters) - } - - override fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - override fun onFetchMangaError(error: Throwable) { - setRefreshing(false) - activity?.toast(error.message) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - override fun setRefreshing(value: Boolean) { - binding.swipeRefresh.isRefreshing = value - } - - override fun onFavoriteClick() { - val manga = presenter.manga - - if (manga.favorite) { - getHeader()?.toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_removed_library)) - } else { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - - when { - // Default category set - defaultCategory != null -> { - getHeader()?.toggleFavorite() - presenter.moveMangaToCategory(manga, defaultCategory) - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - // Automatic 'Default' or no categories - defaultCategoryId == 0 || categories.isEmpty() -> { - getHeader()?.toggleFavorite() - presenter.moveMangaToCategory(manga, null) - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - // Choose a category - else -> { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - } - } - - override fun onCategoriesClick() { - val manga = presenter.manga - val categories = presenter.getCategories() - - 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 - - if (!manga.favorite) { - getHeader()?.toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - presenter.moveMangaToCategories(manga, categories) - } - - /** - * Perform a global search using the provided query. - * - * @param query the search query to pass to the search controller - */ - override fun performGlobalSearch(query: String) { - val router = router ?: return - router.pushController(GlobalSearchController(query).withFadeTransaction()) - } - - private fun setChapterCount(count: Float) { - getHeader()?.setChapterCount(count) - } - - private fun setLastUpdateDate(date: Date) { - getHeader()?.setLastUpdateDate(date) - } - - fun setFavoriteButtonState(isFavorite: Boolean) { - getHeader()?.setFavoriteButtonState(isFavorite) - } - - fun setTrackingIcon(tracked: Boolean) { - getHeader()?.setTrackingIcon(tracked) - } - - override fun isInitialLoadAndFromSource() = fromSource && initialLoad - - override fun removeInitialLoad() { - initialLoad = false - } - - // --> EH - override fun wrapTag(namespace: String, tag: String) = - if (tag.contains(' ')) { - "$namespace:\"$tag$\"" - } else { - "$namespace:$tag$" - } - - private fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim() - - override fun isEHentaiBasedSource(): Boolean { - val sourceId = presenter.source.id - return sourceId == EH_SOURCE_ID || - sourceId == EXH_SOURCE_ID - } - // <-- EH - - /** - * Perform a search using the provided query. - * - * @param query the search query to the previous controller - */ - override fun performSearch(query: String) { - val router = router ?: return - - if (router.backstackSize < 2) { - return - } - - when (val previousController = router.backstack[router.backstackSize - 2].controller()) { - is LibraryController -> { - router.handleBack() - previousController.search(query) - } - is UpdatesController, - is HistoryController -> { - // Manually navigate to LibraryController - router.handleBack() - (router.activity as MainActivity).setSelectedNavItem(R.id.nav_library) - val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController - controller.search(query) - } - is BrowseSourceController -> { - router.handleBack() - previousController.searchWithQuery(query) - } - } - } - - // CHAPTER FUNCTIONS START HERE - override fun onDestroyView(view: View) { - super.onDestroyView(view) - destroyActionModeIfNeeded() - binding.actionToolbar.destroy() - adapter = null - } - - override fun onActivityResumed(activity: Activity) { - if (view == null) return - - // Check if animation view is visible - if (binding.revealView.visibility == View.VISIBLE) { - // Show the unreveal effect - val coordinates = binding.fab.getCoordinates() - binding.revealView.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) - val menuFilterEmpty = menu.findItem(R.id.action_filter_empty) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked() - - if (filterSet) { - val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) - DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor) - } - - // Only show remove filter option if there's a filter set. - menuFilterEmpty.isVisible = filterSet - - // Disable unread filter option if read filter is enabled. - if (presenter.onlyRead()) { - menuFilterUnread.isEnabled = false - } - // Disable read filter option if unread filter is enabled. - if (presenter.onlyUnread()) { - menuFilterRead.isEnabled = false - } - - // Display mode submenu - if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { - menu.findItem(R.id.display_title).isChecked = true - } else { - menu.findItem(R.id.display_chapter_number).isChecked = true - } - - // Sorting mode submenu - val sortingItem = when (presenter.manga.sorting) { - Manga.SORTING_SOURCE -> R.id.sort_by_source - Manga.SORTING_NUMBER -> R.id.sort_by_number - Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date - else -> throw NotImplementedError("Unimplemented sorting method") - } - menu.findItem(sortingItem).isChecked = true - - if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.display_title -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NAME) - } - R.id.display_chapter_number -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NUMBER) - } - - R.id.sort_by_source -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_SOURCE) - } - R.id.sort_by_number -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_NUMBER) - } - R.id.sort_by_upload_date -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_UPLOAD_DATE) - } - - R.id.download_next, R.id.download_next_5, R.id.download_next_10, - R.id.download_custom, R.id.download_unread, R.id.download_all - -> downloadChapters(item.itemId) - - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - R.id.action_edit -> { - editMangaDialog = EditMangaDialog( - this, presenter.manga - ) - editMangaDialog?.showDialog(router) - } - R.id.action_sort -> presenter.revertSortOrder() - } - return super.onOptionsItemSelected(item) - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): MangaAllInOneChapterHolder? { - return binding.recycler.findViewHolderForItemId(chapter.id!!) as? MangaAllInOneChapterHolder - } - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - override fun onItemClick(view: View?, position: Int): Boolean { - val adapter = adapter ?: return false - if (adapter.getItem(position) is MangaAllInOneHeaderItem) return false - val item = adapter.getItem(position) as MangaAllInOneChapterItem? ?: return false - return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - lastClickPosition = position - toggleSelection(position) - true - } else { - openChapter(item.chapter) - false - } - } - - override fun onItemLongClick(position: Int) { - if (adapter?.getItem(position) is MangaAllInOneHeaderItem) return - createActionModeIfNeeded() - when { - lastClickPosition == -1 -> setSelection(position) - lastClickPosition > position -> - for (i in position until lastClickPosition) - setSelection(i) - lastClickPosition < position -> - for (i in lastClickPosition + 1..position) - setSelection(i) - else -> setSelection(position) - } - lastClickPosition = position - adapter?.notifyDataSetChanged() - } - - // SELECTIONS & ACTION MODE - - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) as MangaAllInOneChapterItem? ?: return - adapter.toggleSelection(position) - adapter.notifyDataSetChanged() - if (adapter.isSelected(position)) { - selectedItems.add(item) - } else { - selectedItems.remove(item) - } - actionMode?.invalidate() - } - - private fun setSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) as MangaAllInOneChapterItem? ?: return - if (!adapter.isSelected(position)) { - adapter.toggleSelection(position) - selectedItems.add(item) - actionMode?.invalidate() - } - } - - private fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as MangaAllInOneChapterItem } - } - - private fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) - binding.actionToolbar.show( - actionMode!!, - R.menu.chapter_selection - ) { onActionItemClicked(it!!) } - } - } - - private fun destroyActionModeIfNeeded() { - lastClickPosition = -1 - actionMode?.finish() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.generic_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = count.toString() - - val chapters = getSelectedChapters() - binding.actionToolbar.findItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded } - binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded } - binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } - binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } - binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } - binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } - - // Hide FAB to avoid interfering with the bottom action toolbar - // binding.fab.hide() - binding.fab.gone() - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return onActionItemClicked(item) - } - - private fun onActionItemClicked(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_select_inverse -> selectInverse() - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> showDeleteChaptersConfirmationDialog() - R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true) - R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false) - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_mark_previous_as_read -> markPreviousAsRead(getSelectedChapters()) - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - binding.actionToolbar.hide() - adapter?.mode = SelectableAdapter.Mode.SINGLE - adapter?.clearSelection() - selectedItems.clear() - actionMode = null - - // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] - // fails to show up properly - // binding.fab.show() - binding.fab.visible() - } - - override fun onDetach(view: View) { - destroyActionModeIfNeeded() - super.onDetach(view) - } - - // SELECTION MODE ACTIONS - - private fun selectAll() { - val adapter = adapter ?: return - adapter.selectAll() - selectedItems.addAll(adapter.items) - actionMode?.invalidate() - } - - private fun selectInverse() { - val adapter = adapter ?: return - - selectedItems.clear() - for (i in 0..adapter.itemCount) { - adapter.toggleSelection(i) - } - selectedItems.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) as MangaAllInOneChapterItem }) - - actionMode?.invalidate() - adapter.notifyDataSetChanged() - } - - private fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - destroyActionModeIfNeeded() - } - - private fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - destroyActionModeIfNeeded() - } - - private fun downloadChapters(chapters: List) { - val view = view - presenter.downloadChapters(chapters) - if (view != null && !presenter.manga.favorite) { - binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - } - } - destroyActionModeIfNeeded() - } - - private fun showDeleteChaptersConfirmationDialog() { - DeleteChaptersDialog(this).showDialog(router) - } - - override fun deleteChapters() { - deleteChapters(getSelectedChapters()) - } - - private fun markPreviousAsRead(chapters: List) { - val adapter = adapter ?: return - val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = prevChapters.indexOf(chapters.last()) - if (chapterPos != -1) { - markAsRead(prevChapters.take(chapterPos)) - } - destroyActionModeIfNeeded() - } - - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - presenter.bookmarkChapters(chapters, bookmarked) - destroyActionModeIfNeeded() - } - - fun deleteChapters(chapters: List) { - if (chapters.isEmpty()) return - - presenter.deleteChapters(chapters) - destroyActionModeIfNeeded() - } - - fun onChaptersDeleted(chapters: List) { - // this is needed so the downloaded text gets removed from the item - chapters.forEach { - adapter?.updateItem(it) - } - adapter?.notifyDataSetChanged() - } - - fun onChaptersDeletedError(error: Throwable) { - Timber.e(error) - } - - // OVERFLOW MENU DIALOGS - - private fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - adapter?.notifyDataSetChanged() - } - - private fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - private fun downloadChapters(choice: Int) { - val chaptersToDownload = when (choice) { - R.id.download_next -> getUnreadChaptersSorted().take(1) - R.id.download_next_5 -> getUnreadChaptersSorted().take(5) - R.id.download_next_10 -> getUnreadChaptersSorted().take(10) - R.id.download_custom -> { - showCustomDownloadDialog() - return - } - R.id.download_unread -> presenter.chapters.filter { !it.read } - R.id.download_all -> presenter.chapters - else -> emptyList() - } - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - destroyActionModeIfNeeded() - } - - private fun showCustomDownloadDialog() { - DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router) - } - - override fun downloadCustomChapters(amount: Int) { - val chaptersToDownload = getUnreadChaptersSorted().take(amount) - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } - - fun changeCover() { - if (manga?.favorite == true) { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "image/*" - startActivityForResult( - Intent.createChooser( - intent, - resources?.getString(R.string.select_cover_image) - ), - 101 - ) - } else { - activity?.toast(R.string.cover_must_be_in_library) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 101) { - if (data == null || resultCode != Activity.RESULT_OK) return - val activity = activity ?: return - try { - val uri = data.data ?: return - if (editMangaDialog != null) editMangaDialog?.updateCover(uri) - else { - presenter.editCoverWithStream(uri) - } - } catch (error: IOException) { - activity.toast(R.string.failed_to_update_cover) - Timber.e(error) - } - } - } - - companion object { - // EXH --> - const val UPDATE_EXTRA = "update" - const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig" - // EXH <-- - const val FROM_SOURCE_EXTRA = "from_source" - const val MANGA_EXTRA = "manga" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt deleted file mode 100644 index b900c0a0e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt +++ /dev/null @@ -1,48 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.browse.source.SourceController - -class MangaAllInOneHeaderItem(val manga: Manga, val source: Source, var smartSearchConfig: SourceController.SmartSearchConfig? = null) : - AbstractFlexibleItem() { - - override fun getLayoutRes(): Int { - return R.layout.manga_all_in_one_header - } - - override fun isSelectable(): Boolean { - return false - } - - override fun isSwipeable(): Boolean { - return false - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MangaAllInOneHolder { - return MangaAllInOneHolder(view, adapter as MangaAllInOneAdapter, smartSearchConfig) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: MangaAllInOneHolder, - position: Int, - payloads: MutableList? - ) { - holder.bind(manga, source) - } - - override fun equals(other: Any?): Boolean { - return (this === other) - } - - override fun hashCode(): Int { - return -(manga.id).hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt deleted file mode 100644 index 864088154..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt +++ /dev/null @@ -1,413 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.content.Context -import android.view.View -import androidx.core.content.ContextCompat -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.google.gson.Gson -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.glide.GlideApp -import eu.kanade.tachiyomi.data.glide.toMangaThumbnail -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.databinding.MangaAllInOneHeaderBinding -import eu.kanade.tachiyomi.source.Source -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.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.visible -import eu.kanade.tachiyomi.util.view.visibleIf -import exh.MERGED_SOURCE_ID -import exh.util.setChipsExtended -import java.text.DateFormat -import java.text.DecimalFormat -import java.util.Date -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.android.view.longClicks -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy - -class MangaAllInOneHolder( - view: View, - private val adapter: MangaAllInOneAdapter, - smartSearchConfig: SourceController.SmartSearchConfig? = null -) : BaseFlexibleViewHolder(view, adapter) { - - private val preferences: PreferencesHelper by injectLazy() - - private val gson: Gson by injectLazy() - - private val dateFormat: DateFormat by lazy { - preferences.dateFormat() - } - - private val sourceManager: SourceManager by injectLazy() - - var binding: MangaAllInOneHeaderBinding - - init { - val presenter = adapter.delegate.mangaPresenter() - - binding = MangaAllInOneHeaderBinding.bind(itemView) - - // Setting this via XML doesn't work - binding.mangaCover.clipToOutline = true - - binding.btnFavorite.clicks() - .onEach { adapter.delegate.onFavoriteClick() } - .launchIn(adapter.delegate.controllerScope) - - if ((Injekt.get().hasLoggedServices())) { - binding.btnTracking.visible() - } - - setTrackingIcon( - Injekt.get().getTracks(presenter.manga).executeAsBlocking().any { - val status = Injekt.get().getService(it.sync_id)?.getStatus(it.status) - status != null - } - ) - - binding.btnTracking.clicks() - .onEach { adapter.delegate.openTracking() } - .launchIn(adapter.delegate.controllerScope) - - if (presenter.manga.favorite && presenter.getCategories().isNotEmpty()) { - binding.btnCategories.visible() - } - binding.btnCategories.clicks() - .onEach { adapter.delegate.onCategoriesClick() } - .launchIn(adapter.delegate.controllerScope) - - if (presenter.source is HttpSource) { - binding.btnWebview.visible() - binding.btnShare.visible() - - binding.btnWebview.clicks() - .onEach { adapter.delegate.openInWebView() } - .launchIn(adapter.delegate.controllerScope) - binding.btnShare.clicks() - .onEach { adapter.delegate.shareManga() } - .launchIn(adapter.delegate.controllerScope) - } - - if (presenter.manga.favorite) { - binding.btnMigrate.visible() - binding.btnSmartSearch.visible() - } - - binding.btnMigrate.clicks() - .onEach { - adapter.delegate.migrateManga() - } - .launchIn(adapter.delegate.controllerScope) - - binding.btnSmartSearch.clicks() - .onEach { adapter.delegate.openSmartSearch() } - .launchIn(adapter.delegate.controllerScope) - - binding.mangaFullTitle.longClicks() - .onEach { - adapter.delegate.copyToClipboard(view.context.getString(R.string.title), binding.mangaFullTitle.text.toString()) - } - .launchIn(adapter.delegate.controllerScope) - - binding.mangaFullTitle.clicks() - .onEach { - adapter.delegate.performGlobalSearch(binding.mangaFullTitle.text.toString()) - } - .launchIn(adapter.delegate.controllerScope) - - binding.mangaAuthor.longClicks() - .onEach { - // EXH Special case E-Hentai/ExHentai to ignore author field (unused) - if (!adapter.delegate.isEHentaiBasedSource()) { - adapter.delegate.copyToClipboard("author", binding.mangaAuthor.text.toString()) - } - } - .launchIn(adapter.delegate.controllerScope) - - binding.mangaAuthor.clicks() - .onEach { - // EXH Special case E-Hentai/ExHentai to ignore author field (unused) - if (!adapter.delegate.isEHentaiBasedSource()) { - adapter.delegate.performGlobalSearch(binding.mangaAuthor.text.toString()) - } - } - .launchIn(adapter.delegate.controllerScope) - - binding.mangaSummary.longClicks() - .onEach { - adapter.delegate.copyToClipboard(view.context.getString(R.string.description), binding.mangaSummary.text.toString()) - } - .launchIn(adapter.delegate.controllerScope) - - binding.mangaCover.longClicks() - .onEach { - adapter.delegate.copyToClipboard(view.context.getString(R.string.title), presenter.manga.title) - } - .launchIn(adapter.delegate.controllerScope) - - // EXH --> - if (smartSearchConfig == null) { - binding.recommendBtn.visible() - binding.recommendBtn.clicks() - .onEach { adapter.delegate.openRecommends() } - .launchIn(adapter.delegate.controllerScope) - } - smartSearchConfig?.let { - if (it.origMangaId != null) { binding.mergeBtn.visible() } - binding.mergeBtn.clicks() - .onEach { - adapter.delegate.mergeWithAnother() - } - - .launchIn(adapter.delegate.controllerScope) - } - // EXH <-- - } - - fun bind(manga: Manga, source: Source?) { - binding.mangaFullTitle.text = if (manga.title.isBlank()) { - itemView.context.getString(R.string.unknown) - } else { - manga.title - } - - // Update author/artist TextView. - - val authors: MutableSet = mutableSetOf() - val author = manga.author - val artist = manga.artist - val splitRegex = "([,\\-])".toRegex() - if (author != null) { - authors += author.split(splitRegex).map { it.trim() }.filter { !it.isBlank() }.toMutableSet() - } - if (artist != null) { - authors += artist.split(splitRegex).map { it.trim() }.filter { !it.isBlank() }.toMutableSet() - } - binding.mangaAuthor.text = if (authors.isEmpty()) { - itemView.context.getString(R.string.unknown) - } else { - authors.joinToString(", ") - } - - // If manga source is known update source TextView. - val mangaSource = source?.toString() - with(binding.mangaSource) { - // EXH --> - when { - mangaSource == null -> { - text = itemView.context.getString(R.string.unknown) - } - source.id == MERGED_SOURCE_ID -> { - text = eu.kanade.tachiyomi.source.online.all.MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { - sourceManager.getOrStub(it.source).toString() - }.distinct().joinToString() - } - else -> { - text = mangaSource - setOnClickListener { - val sourceManager = Injekt.get() - adapter.delegate.performSearch(sourceManager.getOrStub(source.id).name) - } - } - } - // EXH <-- - } - - // EXH --> - if (source?.id == MERGED_SOURCE_ID) { - binding.mangaSourceLabel.setText(R.string.label_sources) - } else { - binding.mangaSourceLabel.setText(R.string.manga_info_source_label) - } - // EXH <-- - - // Update status TextView. - binding.mangaStatus.setText( - when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - } - ) - - setChapterCount(0F) - setLastUpdateDate(Date(0L)) - - // Set the favorite drawable to the correct one. - setFavoriteButtonState(manga.favorite) - - // Set cover if it wasn't already. - val mangaThumbnail = manga.toMangaThumbnail() - - GlideApp.with(itemView.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(binding.mangaCover) - - GlideApp.with(itemView.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(binding.backdrop) - - // Manga info section - if (manga.description.isNullOrBlank() && manga.genre.isNullOrBlank()) { - hideMangaInfo() - } else { - // Update description TextView. - binding.mangaSummary.text = if (manga.description.isNullOrBlank()) { - itemView.context.getString(R.string.unknown) - } else { - manga.description - } - - // Update genres list - if (!manga.genre.isNullOrBlank()) { - binding.mangaGenresTagsCompactChips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) - binding.mangaGenresTagsFullChips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) - } else { - binding.mangaGenresTagsWrapper.gone() - } - - // Handle showing more or less info - binding.mangaSummary.clicks() - .onEach { toggleMangaInfo(itemView.context) } - .launchIn(adapter.delegate.controllerScope) - binding.mangaInfoToggle.clicks() - .onEach { toggleMangaInfo(itemView.context) } - .launchIn(adapter.delegate.controllerScope) - - // Expand manga info if navigated from source listing - if (adapter.delegate.isInitialLoadAndFromSource()) { - adapter.delegate.removeInitialLoad() - toggleMangaInfo(itemView.context) - } - } - } - - private fun hideMangaInfo() { - binding.mangaSummaryLabel.gone() - binding.mangaSummary.gone() - binding.mangaGenresTagsWrapper.gone() - binding.mangaInfoToggle.gone() - } - - private fun toggleMangaInfo(context: Context) { - val isExpanded = binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse) - - binding.mangaInfoToggle.text = - if (isExpanded) { - context.getString(R.string.manga_info_expand) - } else { - context.getString(R.string.manga_info_collapse) - } - - with(binding.mangaSummary) { - maxLines = - if (isExpanded) { - 3 - } else { - Int.MAX_VALUE - } - - ellipsize = - if (isExpanded) { - android.text.TextUtils.TruncateAt.END - } else { - null - } - } - - binding.mangaGenresTagsCompact.visibleIf { isExpanded } - binding.mangaGenresTagsFullChips.visibleIf { !isExpanded } - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Float) { - if (count > 0f) { - binding.mangaChapters.text = DecimalFormat("#.#").format(count) - } else { - binding.mangaChapters.text = itemView.context.getString(R.string.unknown) - } - } - - fun setLastUpdateDate(date: Date) { - if (date.time != 0L) { - binding.mangaLastUpdate.text = dateFormat.format(date) - } else { - binding.mangaLastUpdate.text = itemView.context.getString(R.string.unknown) - } - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - fun toggleFavorite() { - val presenter = adapter.delegate.mangaPresenter() - - val isNowFavorite = presenter.toggleFavorite() - if (!isNowFavorite && presenter.hasDownloads()) { - itemView.snack(itemView.context.getString(R.string.delete_downloads_for_manga)) { - setAction(R.string.action_delete) { - presenter.deleteDownloads() - } - } - } - - binding.btnCategories.visibleIf { isNowFavorite && presenter.getCategories().isNotEmpty() } - if (isNowFavorite) { - binding.btnSmartSearch.visible() - binding.btnMigrate.visible() - } else { - binding.btnSmartSearch.gone() - binding.btnMigrate.gone() - } - } - - /** - * Update favorite button with correct drawable and text. - * - * @param isFavorite determines if manga is favorite or not. - */ - fun setFavoriteButtonState(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - binding.btnFavorite.apply { - icon = ContextCompat.getDrawable(context, if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp) - text = context.getString(if (isFavorite) R.string.in_library else R.string.add_to_library) - isChecked = isFavorite - } - } - - private fun performSearch(query: String) { - adapter.delegate.performSearch(query) - } - - private fun performGlobalSearch(query: String) { - adapter.delegate.performGlobalSearch(query) - } - - fun setTrackingIcon(tracked: Boolean) { - if (tracked) { - binding.btnTracking.setIconResource(R.drawable.ic_cloud_24dp) - } - } -} 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 index 218878e6d..0ba528309 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -1,49 +1,108 @@ package eu.kanade.tachiyomi.ui.manga -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.graphics.drawable.DrawableCompat +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import com.bluelinelabs.conductor.Router -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.support.RouterPagerAdapter -import com.google.android.material.tabs.TabLayout -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.snackbar.Snackbar +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.databinding.PagerControllerBinding +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.databinding.ChaptersControllerBinding +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.RxController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController -import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.controller.FabController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.library.ChangeMangaCoverDialog +import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterHolder +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter +import eu.kanade.tachiyomi.ui.manga.chapter.DeleteChaptersDialog +import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog +import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter +import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter import eu.kanade.tachiyomi.ui.manga.track.TrackController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.recent.history.HistoryController +import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController +import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.hasCustomCover +import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.toast -import java.util.Date -import kotlinx.android.synthetic.main.main_activity.tabs -import rx.Subscription +import eu.kanade.tachiyomi.util.view.getCoordinates +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import eu.kanade.tachiyomi.util.view.snack +import eu.kanade.tachiyomi.util.view.visible +import java.io.IOException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import reactivecircus.flowbinding.android.view.clicks +import reactivecircus.flowbinding.swiperefreshlayout.refreshes +import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy -class MangaController : RxController, TabbedController { +class MangaController : + NucleusController, + FabController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ChangeMangaCoverDialog.Listener, + ChangeMangaCategoriesDialog.Listener, + DownloadCustomChaptersDialog.Listener, + DeleteChaptersDialog.Listener { constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: Boolean = false) : super( Bundle().apply { putLong(MANGA_EXTRA, manga?.id ?: 0) putBoolean(FROM_SOURCE_EXTRA, fromSource) + // SY --> putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig) putBoolean(UPDATE_EXTRA, update) + // SY <-- } ) { this.manga = manga @@ -52,20 +111,6 @@ class MangaController : RxController, TabbedController { } } - // EXH --> - constructor(redirect: ChaptersPresenter.EXHRedirect) : super( - Bundle().apply { - putLong(MANGA_EXTRA, redirect.manga.id!!) - putBoolean(UPDATE_EXTRA, redirect.update) - } - ) { - this.manga = redirect.manga - if (manga != null) { - source = Injekt.get().getOrStub(redirect.manga.source) - } - } - // EXH <-- - constructor(mangaId: Long) : this( Injekt.get().getManga(mangaId).executeAsBlocking() ) @@ -79,64 +124,51 @@ class MangaController : RxController, TabbedController { var source: Source? = null private set - private var adapter: MangaDetailAdapter? = null + private val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) - val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) + private val preferences: PreferencesHelper by injectLazy() + private val coverCache: CoverCache by injectLazy() - var update = args.getBoolean(UPDATE_EXTRA, false) + private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null + private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null + private var chaptersAdapter: ChaptersAdapter? = null + + private var actionFab: ExtendedFloatingActionButton? = null + private var actionFabScrollListener: RecyclerView.OnScrollListener? = null + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Selected items. Used to restore selections after a rotation. + */ + private val selectedChapters = mutableSetOf() + + private val isLocalSource by lazy { presenter.source.id == LocalSource.ID } + + private var lastClickPosition = -1 + + private var isRefreshingInfo = false + private var isRefreshingChapters = false // EXH --> - val smartSearchConfig: SourceController.SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG_EXTRA) + val smartSearchConfig: SourceController.SmartSearchConfig? = args.getParcelable( + SMART_SEARCH_CONFIG_EXTRA + ) + + private var editMangaDialog: EditMangaDialog? = null // EXH <-- - val lastUpdateRelay: BehaviorRelay = BehaviorRelay.create() - - val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() - - val mangaFavoriteRelay: PublishRelay = PublishRelay.create() - - private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() - - private var trackingIconSubscription: Subscription? = null + init { + setHasOptionsMenu(true) + } override fun getTitle(): String? { return manga?.title } - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = PagerControllerBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - if (manga == null || source == null) return - - requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) - - adapter = MangaDetailAdapter() - binding.pager.offscreenPageLimit = 3 - binding.pager.adapter = adapter - - if (!fromSource) { - binding.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(binding.pager) - trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } - } - } - override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeEnded(handler, type) if (manga == null || source == null) { @@ -145,75 +177,969 @@ class MangaController : RxController, TabbedController { } } - 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 - } - - tab.icon = drawable - } - - private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { - - private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 - - private val tabTitles = listOf( - R.string.manga_detail_tab, - R.string.manga_chapters_tab, - R.string.manga_tracking_tab + override fun createPresenter(): MangaPresenter { + return MangaPresenter( + manga!!, + source!! ) - .map { resources!!.getString(it) } + } - override fun getCount(): Int { - return tabCount - } + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + binding = ChaptersControllerBinding.inflate(inflater) + return binding.root + } - override fun configureRouter(router: Router, position: Int) { - if (!router.hasRootController()) { - val controller = when (position) { - INFO_CONTROLLER -> MangaInfoController(fromSource) - CHAPTERS_CONTROLLER -> ChaptersController() - TRACK_CONTROLLER -> TrackController() - else -> error("Wrong position $position") - } - router.setRoot(RouterTransaction.with(controller)) + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + if (manga == null || source == null) return + + // Init RecyclerView and adapter + mangaInfoAdapter = MangaInfoHeaderAdapter(this, fromSource) + chaptersHeaderAdapter = MangaChaptersHeaderAdapter() + chaptersAdapter = ChaptersAdapter(this, view.context) + + binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) + binding.recycler.layoutManager = LinearLayoutManager(view.context) + binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recycler.setHasFixedSize(true) + chaptersAdapter?.fastScroller = binding.fastScroller + + actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) + + // Skips directly to chapters list if navigated to from the library + binding.recycler.post { + if (!fromSource && preferences.jumpToChapters()) { + (binding.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0) } } - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] + binding.swipeRefresh.refreshes() + .onEach { + fetchMangaInfoFromSource(manualFetch = true) + fetchChaptersFromSource(manualFetch = true) + } + .launchIn(scope) + + binding.actionToolbar.offsetAppbarHeight(activity!!) + } + + override fun configureFab(fab: ExtendedFloatingActionButton) { + actionFab = fab + fab.setText(R.string.action_start) + fab.setIconResource(R.drawable.ic_play_arrow_24dp) + fab.clicks() + .onEach { + 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 + actionFab?.getCoordinates()?.let { coordinates -> + if (!binding.revealView.showRevealEffect( + coordinates.x, + coordinates.y, + revealAnimationListener + ) + ) { + openChapter(item.chapter) + } + } + } else { + view?.context?.toast(R.string.no_next_chapter) + } + } + .launchIn(scope) + } + + override fun cleanupFab(fab: ExtendedFloatingActionButton) { + actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } + actionFab = null + } + + override fun onDestroyView(view: View) { + destroyActionModeIfNeeded() + binding.actionToolbar.destroy() + mangaInfoAdapter = null + chaptersHeaderAdapter = null + chaptersAdapter = null + super.onDestroyView(view) + } + + override fun onActivityResumed(activity: Activity) { + if (view == null) return + + // Check if animation view is visible + if (binding.revealView.visibility == View.VISIBLE) { + // Show the unreveal effect + actionFab?.getCoordinates()?.let { coordinates -> + binding.revealView.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) + val menuFilterEmpty = menu.findItem(R.id.action_filter_empty) + + // Set correct checkbox values. + menuFilterRead.isChecked = presenter.onlyRead() + menuFilterUnread.isChecked = presenter.onlyUnread() + menuFilterDownloaded.isChecked = presenter.onlyDownloaded() + menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() + menuFilterBookmarked.isChecked = presenter.onlyBookmarked() + + val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked() + if (filterSet) { + val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) + DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor) + } + + // Only show remove filter option if there's a filter set. + menuFilterEmpty.isVisible = filterSet + + // Display mode submenu + if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { + menu.findItem(R.id.display_title).isChecked = true + } else { + menu.findItem(R.id.display_chapter_number).isChecked = true + } + + // Sorting mode submenu + val sortingItem = when (presenter.manga.sorting) { + Manga.SORTING_SOURCE -> R.id.sort_by_source + Manga.SORTING_NUMBER -> R.id.sort_by_number + Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date + else -> throw NotImplementedError("Unimplemented sorting method") + } + menu.findItem(sortingItem).isChecked = true + menu.findItem(R.id.action_sort_descending).isChecked = presenter.manga.sortDescending() + + // Hide download options for local manga + menu.findItem(R.id.download_group).isVisible = !isLocalSource + + // Hide edit cover and migrate options for non-library manga + menu.findItem(R.id.action_edit_cover).isVisible = presenter.manga.favorite + /* SY --> menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite SY <-- */ + + // SY --> + if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true + // SY <-- + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.display_title -> { + item.isChecked = true + setDisplayMode(Manga.DISPLAY_NAME) + } + R.id.display_chapter_number -> { + item.isChecked = true + setDisplayMode(Manga.DISPLAY_NUMBER) + } + + R.id.sort_by_source -> { + item.isChecked = true + presenter.setSorting(Manga.SORTING_SOURCE) + } + R.id.sort_by_number -> { + item.isChecked = true + presenter.setSorting(Manga.SORTING_NUMBER) + } + R.id.sort_by_upload_date -> { + item.isChecked = true + presenter.setSorting(Manga.SORTING_UPLOAD_DATE) + } + R.id.action_sort_descending -> { + presenter.reverseSortOrder() + activity?.invalidateOptionsMenu() + } + + R.id.download_next, R.id.download_next_5, R.id.download_next_10, + R.id.download_custom, R.id.download_unread, R.id.download_all + -> downloadChapters(item.itemId) + + R.id.action_filter_unread -> { + item.isChecked = !item.isChecked + presenter.setUnreadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_read -> { + item.isChecked = !item.isChecked + presenter.setReadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_downloaded -> { + item.isChecked = !item.isChecked + presenter.setDownloadedFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_bookmarked -> { + item.isChecked = !item.isChecked + presenter.setBookmarkedFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_empty -> { + presenter.removeFilters() + activity?.invalidateOptionsMenu() + } + // SY --> + R.id.action_edit -> { + editMangaDialog = EditMangaDialog( + this, presenter.manga + ) + editMangaDialog?.showDialog(router) + } + // SY <-- + + R.id.action_edit_cover -> handleChangeCover() + // SY --> R.id.action_migrate -> migrateManga() // SY <-- + } + return super.onOptionsItemSelected(item) + } + + private fun updateRefreshing() { + binding.swipeRefresh.isRefreshing = isRefreshingInfo || isRefreshingChapters + } + + // Manga info - start + + /** + * Check if manga is initialized. + * If true update header with manga information, + * if false fetch manga information + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun onNextMangaInfo(manga: Manga, source: Source) { + if (manga.initialized) { + // Update view. + mangaInfoAdapter?.update(manga, source) + } else { + // Initialize manga. + fetchMangaInfoFromSource() } } + /** + * Start fetching manga information from source. + */ + private fun fetchMangaInfoFromSource(manualFetch: Boolean = false) { + isRefreshingInfo = true + updateRefreshing() + + // Call presenter and start fetching manga information + presenter.fetchMangaFromSource(manualFetch) + } + + fun onFetchMangaInfoDone() { + isRefreshingInfo = false + updateRefreshing() + } + + fun onFetchMangaInfoError(error: Throwable) { + isRefreshingInfo = false + updateRefreshing() + activity?.toast(error.message) + } + + fun openMangaInWebView() { + val source = presenter.source as? HttpSource ?: return + + val url = try { + source.mangaDetailsRequest(presenter.manga).url.toString() + } catch (e: Exception) { + return + } + + val activity = activity ?: return + val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title) + startActivity(intent) + } + + fun shareManga() { + val context = view?.context ?: return + + val source = presenter.source as? HttpSource ?: return + try { + val url = source.mangaDetailsRequest(presenter.manga).url.toString() + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + fun onFavoriteClick() { + val manga = presenter.manga + + if (manga.favorite) { + toggleFavorite() + activity?.toast(activity?.getString(R.string.manga_removed_library)) + } else { + addToLibrary(manga) + } + + // Update menu to show migrate option + activity?.invalidateOptionsMenu() + } + + fun onTrackingClick() { + router.pushController(TrackController(manga).withFadeTransaction()) + } + + private fun addToLibrary(manga: Manga) { + val categories = presenter.getCategories() + val defaultCategoryId = preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } + + when { + // Default category set + defaultCategory != null -> { + toggleFavorite() + presenter.moveMangaToCategory(manga, defaultCategory) + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + toggleFavorite() + presenter.moveMangaToCategory(manga, null) + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + + // Choose a category + else -> { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + } + + // SY --> + fun changeCover() { + if (manga?.favorite == true) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + startActivityForResult( + Intent.createChooser( + intent, + resources?.getString(R.string.select_cover_image) + ), + REQUEST_EDIT_MANGA_COVER + ) + } else { + activity?.toast(R.string.cover_must_be_in_library) + } + } + + fun setRefreshing() { + isRefreshingInfo = true + updateRefreshing() + } + // SY <-- + + // EXH --> + fun openSmartSearch() { + val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.title, presenter.manga.id) + + router?.pushController( + SourceController( + Bundle().apply { + putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig) + } + ).withFadeTransaction() + ) + } + + suspend fun mergeWithAnother() { + try { + val mergedManga = withContext(Dispatchers.IO + NonCancellable) { + presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!) + } + + router?.pushController( + MangaController( + mergedManga, + true, + update = true + ).withFadeTransaction() + ) + applicationContext?.toast("Manga merged!") + } catch (e: Exception) { + if (e is CancellationException) throw e + else { + applicationContext?.toast("Failed to merge manga: ${e.message}") + } + } + } + // EXH <-- + + // AZ --> + fun openRecommends() { + val recommendsConfig = BrowseSourceController.RecommendsConfig(presenter.manga) + + router?.pushController( + BrowseSourceController( + Bundle().apply { + putParcelable(BrowseSourceController.RECOMMENDS_CONFIG, recommendsConfig) + } + ).withFadeTransaction() + ) + } + // AZ <-- + + /** + * Toggles the favorite status and asks for confirmation to delete downloaded chapters. + */ + private fun toggleFavorite() { + val isNowFavorite = presenter.toggleFavorite() + if (activity != null && !isNowFavorite && presenter.hasDownloads()) { + activity!!.findViewById(R.id.root_coordinator)?.snack(activity!!.getString(R.string.delete_downloads_for_manga)) { + setAction(R.string.action_delete) { + presenter.deleteDownloads() + } + } + } + + mangaInfoAdapter?.notifyDataSetChanged() + } + + fun onCategoriesClick() { + val manga = presenter.manga + val categories = presenter.getCategories() + + 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 + + if (!manga.favorite) { + toggleFavorite() + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + + presenter.moveMangaToCategories(manga, categories) + } + + /** + * Perform a global search using the provided query. + * + * @param query the search query to pass to the search controller + */ + fun performGlobalSearch(query: String) { + router.pushController(GlobalSearchController(query).withFadeTransaction()) + } + + /** + * Perform a search using the provided query. + * + * @param query the search query to the parent controller + */ + fun performSearch(query: String) { + if (router.backstackSize < 2) { + return + } + + when (val previousController = router.backstack[router.backstackSize - 2].controller()) { + is LibraryController -> { + router.handleBack() + previousController.search(query) + } + is UpdatesController, + is HistoryController -> { + // Manually navigate to LibraryController + router.handleBack() + (router.activity as MainActivity).setSelectedNavItem(R.id.nav_library) + val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController + controller.search(query) + } + is BrowseSourceController -> { + router.handleBack() + previousController.searchWithQuery(query) + } + } + } + + private fun handleChangeCover() { + val manga = manga ?: return + if (manga.hasCustomCover(coverCache)) { + showEditCoverDialog(manga) + } else { + openMangaCoverPicker(manga) + } + } + + /** + * Edit custom cover for selected manga. + */ + private fun showEditCoverDialog(manga: Manga) { + ChangeMangaCoverDialog(this, manga).showDialog(router) + } + + override fun openMangaCoverPicker(manga: 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) + } + + destroyActionModeIfNeeded() + } + + override fun deleteMangaCover(manga: Manga) { + presenter.deleteCustomCover(manga) + mangaInfoAdapter?.notifyDataSetChanged() + destroyActionModeIfNeeded() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_IMAGE_OPEN) { + val dataUri = data?.data + if (dataUri == null || resultCode != Activity.RESULT_OK) return + val activity = activity ?: return + presenter.editCover(manga!!, activity, dataUri) + } + // SY --> + if (requestCode == REQUEST_EDIT_MANGA_COVER) { + if (data == null || resultCode != Activity.RESULT_OK) return + val activity = activity ?: return + try { + val uri = data.data ?: return + if (editMangaDialog != null) editMangaDialog?.updateCover(uri) + else { + presenter.editCoverWithStream(uri) + } + } catch (error: IOException) { + activity.toast(R.string.failed_to_update_cover) + Timber.e(error) + } + } + // SY <-- + } + + fun onSetCoverSuccess() { + mangaInfoAdapter?.notifyDataSetChanged() + activity?.toast(R.string.cover_updated) + } + + fun onSetCoverError(error: Throwable) { + activity?.toast(R.string.notification_cover_update_failed) + Timber.e(error) + } + + /** + * Initiates source migration for the specific manga. + */ + /* SY private */fun migrateManga() { + // SY --> + PreMigrationController.navigateToMigration( + preferences.skipPreMigration().get(), + router, + listOf(presenter.manga.id!!) + ) + // SY <-- + } + + // Manga info - end + + // Chapters list - start + + fun onNextChapters(chapters: List) { + // If the list is empty and it hasn't requested previously, fetch chapters from source + // We use presenter chapters instead because they are always unfiltered + if (!presenter.hasRequested && presenter.chapters.isEmpty()) { + fetchChaptersFromSource() + } + + val chaptersHeader = chaptersHeaderAdapter ?: return + chaptersHeader.setNumChapters(chapters.size) + + val adapter = chaptersAdapter ?: return + adapter.updateDataSet(chapters) + + if (selectedChapters.isNotEmpty()) { + adapter.clearSelection() // we need to start from a clean state, index may have changed + createActionModeIfNeeded() + selectedChapters.forEach { item -> + val position = adapter.indexOf(item) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + } + } + actionMode?.invalidate() + } + + val context = view?.context + if (context != null && chapters.any { it.read }) { + actionFab?.text = context.getString(R.string.action_resume) + } + } + + private fun fetchChaptersFromSource(manualFetch: Boolean = false) { + isRefreshingChapters = true + updateRefreshing() + + presenter.fetchChaptersFromSource(manualFetch) + } + + fun onFetchChaptersDone() { + isRefreshingChapters = false + updateRefreshing() + } + + fun onFetchChaptersError(error: Throwable) { + isRefreshingChapters = false + updateRefreshing() + activity?.toast(error.message) + } + + fun onChapterStatusChange(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status) + } + + private fun getHolder(chapter: Chapter): ChapterHolder? { + return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + } + + fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) + if (hasAnimation) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + } + startActivity(intent) + } + + override fun onItemClick(view: View?, position: Int): Boolean { + val adapter = chaptersAdapter ?: return false + val item = adapter.getItem(position) ?: return false + return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { + lastClickPosition = position + toggleSelection(position) + true + } else { + openChapter(item.chapter) + false + } + } + + override fun onItemLongClick(position: Int) { + createActionModeIfNeeded() + when { + lastClickPosition == -1 -> setSelection(position) + lastClickPosition > position -> + for (i in position until lastClickPosition) + setSelection(i) + lastClickPosition < position -> + for (i in lastClickPosition + 1..position) + setSelection(i) + else -> setSelection(position) + } + lastClickPosition = position + chaptersAdapter?.notifyDataSetChanged() + } + + // SELECTIONS & ACTION MODE + + private fun toggleSelection(position: Int) { + val adapter = chaptersAdapter ?: return + val item = adapter.getItem(position) ?: return + adapter.toggleSelection(position) + adapter.notifyDataSetChanged() + if (adapter.isSelected(position)) { + selectedChapters.add(item) + } else { + selectedChapters.remove(item) + } + actionMode?.invalidate() + } + + private fun setSelection(position: Int) { + val adapter = chaptersAdapter ?: return + val item = adapter.getItem(position) ?: return + if (!adapter.isSelected(position)) { + adapter.toggleSelection(position) + selectedChapters.add(item) + actionMode?.invalidate() + } + } + + private fun getSelectedChapters(): List { + val adapter = chaptersAdapter ?: return emptyList() + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } + } + + private fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) + binding.actionToolbar.show( + actionMode!!, + R.menu.chapter_selection + ) { onActionItemClicked(it!!) } + } + } + + private fun destroyActionModeIfNeeded() { + lastClickPosition = -1 + actionMode?.finish() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.generic_selection, menu) + chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = chaptersAdapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = count.toString() + + val chapters = getSelectedChapters() + binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && chapters.any { !it.isDownloaded } + binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && chapters.any { it.isDownloaded } + binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } + binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } + binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } + binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } + + // Hide FAB to avoid interfering with the bottom action toolbar + // actionFab?.hide() + actionFab?.gone() + } + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return onActionItemClicked(item) + } + + private fun onActionItemClicked(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_select_all -> selectAll() + R.id.action_select_inverse -> selectInverse() + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> showDeleteChaptersConfirmationDialog() + R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true) + R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false) + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_mark_previous_as_read -> markPreviousAsRead(getSelectedChapters()) + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + binding.actionToolbar.hide() + chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE + chaptersAdapter?.clearSelection() + selectedChapters.clear() + actionMode = null + + // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] + // fails to show up properly + // actionFab?.show() + actionFab?.visible() + } + + override fun onDetach(view: View) { + destroyActionModeIfNeeded() + super.onDetach(view) + } + + // SELECTION MODE ACTIONS + + private fun selectAll() { + val adapter = chaptersAdapter ?: return + adapter.selectAll() + selectedChapters.addAll(adapter.items) + actionMode?.invalidate() + } + + private fun selectInverse() { + val adapter = chaptersAdapter ?: return + + selectedChapters.clear() + for (i in 0..adapter.itemCount) { + adapter.toggleSelection(i) + } + selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) + + actionMode?.invalidate() + adapter.notifyDataSetChanged() + } + + private fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + destroyActionModeIfNeeded() + } + + private fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + destroyActionModeIfNeeded() + } + + private fun downloadChapters(chapters: List) { + val view = view + val manga = presenter.manga + presenter.downloadChapters(chapters) + if (view != null && !manga.favorite) { + binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.action_add) { + addToLibrary(manga) + } + } + } + destroyActionModeIfNeeded() + } + + private fun showDeleteChaptersConfirmationDialog() { + DeleteChaptersDialog(this).showDialog(router) + } + + override fun deleteChapters() { + deleteChapters(getSelectedChapters()) + } + + private fun markPreviousAsRead(chapters: List) { + val adapter = chaptersAdapter ?: return + val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items + val chapterPos = prevChapters.indexOf(chapters.last()) + if (chapterPos != -1) { + markAsRead(prevChapters.take(chapterPos)) + } + destroyActionModeIfNeeded() + } + + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + presenter.bookmarkChapters(chapters, bookmarked) + destroyActionModeIfNeeded() + } + + fun deleteChapters(chapters: List) { + if (chapters.isEmpty()) return + + presenter.deleteChapters(chapters) + destroyActionModeIfNeeded() + } + + fun onChaptersDeleted(chapters: List) { + // this is needed so the downloaded text gets removed from the item + chapters.forEach { + chaptersAdapter?.updateItem(it) + } + chaptersAdapter?.notifyDataSetChanged() + } + + fun onChaptersDeletedError(error: Throwable) { + Timber.e(error) + } + + // OVERFLOW MENU DIALOGS + + private fun setDisplayMode(id: Int) { + presenter.setDisplayMode(id) + chaptersAdapter?.notifyDataSetChanged() + } + + private fun getUnreadChaptersSorted() = presenter.chapters + .filter { !it.read && it.status == Download.NOT_DOWNLOADED } + .distinctBy { it.name } + .sortedByDescending { it.source_order } + + private fun downloadChapters(choice: Int) { + val chaptersToDownload = when (choice) { + R.id.download_next -> getUnreadChaptersSorted().take(1) + R.id.download_next_5 -> getUnreadChaptersSorted().take(5) + R.id.download_next_10 -> getUnreadChaptersSorted().take(10) + R.id.download_custom -> { + showCustomDownloadDialog() + return + } + R.id.download_unread -> presenter.chapters.filter { !it.read } + R.id.download_all -> presenter.chapters + else -> emptyList() + } + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + destroyActionModeIfNeeded() + } + + private fun showCustomDownloadDialog() { + DownloadCustomChaptersDialog( + this, + presenter.chapters.size + ).showDialog(router) + } + + override fun downloadCustomChapters(amount: Int) { + val chaptersToDownload = getUnreadChaptersSorted().take(amount) + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + } + + // Chapters list - end + companion object { + const val FROM_SOURCE_EXTRA = "from_source" + const val MANGA_EXTRA = "manga" + // EXH --> const val UPDATE_EXTRA = "update" const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig" // EXH <-- - const val FROM_SOURCE_EXTRA = "from_source" - const val MANGA_EXTRA = "manga" - const val INFO_CONTROLLER = 0 - const val CHAPTERS_CONTROLLER = 1 - const val TRACK_CONTROLLER = 2 + /** + * Key to change the cover of a manga in [onActivityResult]. + */ + const val REQUEST_IMAGE_OPEN = 101 + + const val REQUEST_EDIT_MANGA_COVER = 201 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt similarity index 64% rename from app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 842a75965..c5581f6dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -1,9 +1,11 @@ package eu.kanade.tachiyomi.ui.manga +import android.content.Context import android.net.Uri import android.os.Bundle import com.google.gson.Gson 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 @@ -16,19 +18,17 @@ import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter -import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterItem +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.isLocal +import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed +import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.updateCoverLastModified -import exh.EH_SOURCE_ID -import exh.EXH_SOURCE_ID import exh.MERGED_SOURCE_ID import exh.debug.DebugToggles import exh.eh.EHentaiUpdateHelper @@ -36,11 +36,7 @@ import exh.isEhBasedSource import exh.util.await import exh.util.trimOrNull import java.util.Date -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import rx.Observable import rx.Subscription @@ -51,36 +47,35 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -class MangaAllInOnePresenter( - val controller: MangaAllInOneController, +class MangaPresenter( val manga: Manga, val source: Source, - val smartSearchConfig: SourceController.SmartSearchConfig?, + val preferences: PreferencesHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), - private val gson: Gson = Injekt.get(), - val preferences: PreferencesHelper = Injekt.get() -) : BasePresenter() { + // SY --> + private val gson: Gson = Injekt.get() + // SY <-- +) : BasePresenter() { + + /** + * Subscription to update the manga from the source. + */ + private var fetchMangaSubscription: Subscription? = null /** * List of chapters of the manga. It's always unfiltered and unsorted. */ - var chapters: List = emptyList() + var chapters: List = emptyList() private set - private var lastUpdateDate: Date = Date(0L) - - private var chapterCount: Float = 0F - - private val scope = CoroutineScope(Job() + Dispatchers.Default) - - private val customMangaManager: CustomMangaManager by injectLazy() + /** + * Subject of list of chapters to allow updating the view without going to DB. + */ + private val chaptersRelay: PublishRelay> by lazy { + PublishRelay.create>() + } /** * Whether the chapter list has been requested to the source. @@ -88,120 +83,124 @@ class MangaAllInOnePresenter( 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 // EXH --> + private val customMangaManager: CustomMangaManager by injectLazy() + private val updateHelper: EHentaiUpdateHelper by injectLazy() - private val redirectUserRelay = BehaviorRelay.create() - // EXH <-- + private val redirectUserRelay = BehaviorRelay.create() - var headerItem = MangaAllInOneHeaderItem(manga, source, smartSearchConfig) + data class EXHRedirect(val manga: Manga, val update: Boolean) + // EXH <-- override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - updateManga() + // Manga info - start - // Listen for download status changes - observeDownloads() + getMangaObservable() + .subscribeLatestCache({ view, manga -> view.onNextMangaInfo(manga, source) }) + // Prepare the relay. + chaptersRelay.flatMap { applyChapterFilters(it) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(MangaController::onNextChapters) { _, error -> Timber.e(error) } + + // Manga info - end + + // Chapters list - start + + // 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().subscribe { - scope.launch(Dispatchers.IO) { - updateChaptersView(updateInfo = true) + db.getChapters(manga).asRxObservable() + .map { chapters -> + // Convert every chapter to a model. + chapters.map { it.toModel() } } - } - ) - } + .doOnNext { chapters -> + // Find downloaded chapters + setDownloadedChapters(chapters) - private suspend fun updateChapters() { - val chapters = db.getChapters(manga).await().map { it.toModel() } + // Store the last emission + this.chapters = chapters - // Find downloaded chapters - setDownloadedChapters(chapters) + // Listen for download status changes + observeDownloads() - // EXH --> - if (chapters.isNotEmpty() && (source.isEhBasedSource()) && DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled) { - // Check for gallery in library and accept manga with lowest id - // Find chapters sharing same root - add( - updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters) - .subscribeOn(Schedulers.io()) - .subscribe { (acceptedChain, _) -> - // Redirect if we are not the accepted root - if (manga.id != acceptedChain.manga.id) { - // Update if any of our chapters are not in accepted manga's chapters - val ourChapterUrls = chapters.map { it.url }.toSet() - val acceptedChapterUrls = acceptedChain.chapters.map { it.url }.toSet() - val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty() - redirectUserRelay.call( - ChaptersPresenter.EXHRedirect( - acceptedChain.manga, - update - ) - ) - } + // SY --> + if (chapters.isNotEmpty() && (source.isEhBasedSource()) && DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled) { + // Check for gallery in library and accept manga with lowest id + // Find chapters sharing same root + add( + updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters) + .subscribeOn(Schedulers.io()) + .subscribe { (acceptedChain, _) -> + // Redirect if we are not the accepted root + if (manga.id != acceptedChain.manga.id) { + // Update if any of our chapters are not in accepted manga's chapters + val ourChapterUrls = chapters.map { it.url }.toSet() + val acceptedChapterUrls = acceptedChain.chapters.map { it.url }.toSet() + val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty() + redirectUserRelay.call( + EXHRedirect( + acceptedChain.manga, + update + ) + ) + } + } + ) } + // SY <-- + } + .subscribe { chaptersRelay.call(it) } + ) + + // Chapters list - end + } + + // Manga info - start + + private fun getMangaObservable(): Observable { + return db.getManga(manga.url, manga.source).asRxObservable() + .observeOn(AndroidSchedulers.mainThread()) + } + + /** + * Fetch manga information from source. + */ + fun fetchMangaFromSource(manualFetch: Boolean = false) { + if (!fetchMangaSubscription.isNullOrUnsubscribed()) return + fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } + .map { networkManga -> + manga.prepUpdateCover(coverCache, networkManga, manualFetch) + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + manga + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, _ -> + view.onFetchMangaInfoDone() + }, + MangaController::onFetchMangaInfoError ) - } - // EXH <-- - - this.chapters = chapters - } - - private fun getUpdatedChapters(): List = applyChapterFilters(chapters) - - private fun updateChaptersView(updateInfo: Boolean = false) { - scope.launch(Dispatchers.IO) { - updateChapters() - val chapterList = getUpdatedChapters() - if (updateInfo) { - updateChapterInfo() - } - withContext(Dispatchers.Main) { - Observable.just(manga) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source, chapterList, lastUpdateDate, chapterCount) }) - } - } - } - - private fun updateChapterInfo() { - scope.launch(Dispatchers.IO) { - lastUpdateDate = Date( - chapters.maxBy { it.date_upload }?.date_upload ?: 0 - ) - - chapterCount = chapters.maxBy { it.chapter_number }?.chapter_number ?: 0f - } - } - - private fun updateManga(updateInfo: Boolean = true) { - scope.launch(Dispatchers.IO) { - var manga2: Manga? = null - var chapterList = getUpdatedChapters() - if (updateInfo) { - manga2 = db.getManga(manga.url, manga.source).await() - updateChapters() - updateChapterInfo() - chapterList = getUpdatedChapters() - } - - withContext(Dispatchers.Main) { - if (manga2 != null) { - Observable.just(manga2) - } else { - Observable.just(manga) - }.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source, chapterList, lastUpdateDate, chapterCount) }) - } - } } + // SY --> fun updateMangaInfo( title: String?, author: String?, @@ -240,14 +239,28 @@ class MangaAllInOnePresenter( if (uri != null) { editCoverWithStream(uri) } else if (resetCover) { - controller.setRefreshing(true) coverCache.deleteCustomCover(manga) } if (uri == null && resetCover) { - fetchMangaFromSource(manualFetch = true, fetchChapters = false) + Observable.just(manga) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache( + { view, _ -> + view.setRefreshing() + } + ) + fetchMangaFromSource(manualFetch = true) } else { - updateManga(updateInfo = false) + Observable.just(manga) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache( + { view, _ -> + view.onNextMangaInfo(manga, source) + } + ) } } @@ -268,136 +281,6 @@ class MangaAllInOnePresenter( return false } - /** - * Fetch manga information from source. - */ - fun fetchMangaFromSource(manualFetch: Boolean = false, fetchManga: Boolean = true, fetchChapters: Boolean = true) { - if (fetchChapters) { - hasRequested = true - } - - scope.launch(Dispatchers.IO) { - if (fetchManga) { - val networkManga = try { - source.fetchMangaDetails(manga).toBlocking().single() - } catch (e: Exception) { - controller.onFetchMangaError(e) - return@launch - } - if (networkManga != null) { - manga.prepUpdateCover(coverCache, networkManga, manualFetch) - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).await() - } - } - var chapters: List = listOf() - if (fetchChapters) { - try { - chapters = source.fetchChapterList(manga).toBlocking().single() - } catch (e: Exception) { - controller.onFetchMangaError(e) - return@launch - } - } - try { - if (fetchChapters) { - val chapterLists = syncChaptersWithSource(db, chapters, manga, source) - - if (manualFetch) { - downloadNewChapters(chapterLists.first) - } - - updateChapters() - updateChapterInfo() - } - withContext(Dispatchers.Main) { - updateManga(updateInfo = false) - controller.onFetchMangaDone() - } - } catch (e: Exception) { - controller.onFetchMangaError(e) - } - } - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - * - * @return the new status of the manga. - */ - fun toggleFavorite(): Boolean { - manga.favorite = !manga.favorite - controller.setFavoriteButtonState(manga.favorite) - if (!manga.favorite) { - manga.removeCovers(coverCache) - } - db.insertManga(manga).executeAsBlocking() - return manga.favorite - } - - private fun setFavorite(favorite: Boolean) { - if (manga.favorite == favorite) { - return - } - toggleFavorite() - } - - /** - * Returns true if the manga has any downloads. - */ - fun hasDownloads(): Boolean { - return downloadManager.getDownloadCount(manga) > 0 - } - - /** - * Deletes all the downloads for the manga. - */ - fun deleteDownloads() { - downloadManager.deleteManga(manga, source) - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - fun getCategories(): List { - return db.getCategories().executeAsBlocking() - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param manga the manga to move. - * @param categories the selected categories. - */ - fun moveMangaToCategories(manga: Manga, categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param manga the manga to move. - * @param category the selected category, or null for default category. - */ - fun moveMangaToCategory(manga: Manga, category: Category?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga { val originalManga = db.getManga(originalMangaId).await() ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId") @@ -458,6 +341,126 @@ class MangaAllInOnePresenter( return toInsert } + // SY <-- + + /** + * Update favorite status of manga, (removes / adds) manga (to / from) library. + * + * @return the new status of the manga. + */ + fun toggleFavorite(): Boolean { + manga.favorite = !manga.favorite + manga.date_added = when (manga.favorite) { + true -> Date().time + false -> 0 + } + if (!manga.favorite) { + manga.removeCovers(coverCache) + } + db.insertManga(manga).executeAsBlocking() + return manga.favorite + } + + /** + * Returns true if the manga has any downloads. + */ + fun hasDownloads(): Boolean { + return downloadManager.getDownloadCount(manga) > 0 + } + + /** + * Deletes all the downloads for the manga. + */ + fun deleteDownloads() { + downloadManager.deleteManga(manga, source) + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + fun getCategories(): List { + return db.getCategories().executeAsBlocking() + } + + /** + * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. + * + * @param manga the manga to get categories from. + * @return Array of category ids the manga is in, if none returns default id + */ + fun getMangaCategoryIds(manga: Manga): Array { + val categories = db.getCategoriesForManga(manga).executeAsBlocking() + return categories.mapNotNull { it.id }.toTypedArray() + } + + /** + * Move the given manga to categories. + * + * @param manga the manga to move. + * @param categories the selected categories. + */ + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) + } + + /** + * Move the given manga to the category. + * + * @param manga the manga to move. + * @param category the selected category, or null for default category. + */ + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) + } + + /** + * Update cover with local file. + * + * @param manga the manga edited. + * @param context Context. + * @param data uri of the cover resource. + */ + fun editCover(manga: Manga, context: Context, data: Uri) { + Observable + .fromCallable { + context.contentResolver.openInputStream(data)?.use { + if (manga.isLocal()) { + LocalSource.updateCover(context, manga, it) + manga.updateCoverLastModified(db) + } else if (manga.favorite) { + coverCache.setCustomCoverToCache(manga, it) + manga.updateCoverLastModified(db) + } + } + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, _ -> view.onSetCoverSuccess() }, + { view, e -> view.onSetCoverError(e) } + ) + } + + fun deleteCustomCover(manga: Manga) { + Observable + .fromCallable { + coverCache.deleteCustomCover(manga) + manga.updateCoverLastModified(db) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, _ -> view.onSetCoverSuccess() }, + { view, e -> view.onSetCoverError(e) } + ) + } + + // Manga info - end + + // Chapters list - start private fun observeDownloads() { observeDownloadsSubscription?.let { remove(it) } @@ -465,7 +468,7 @@ class MangaAllInOnePresenter( .observeOn(AndroidSchedulers.mainThread()) .filter { download -> download.manga.id == manga.id } .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(MangaAllInOneController::onChapterStatusChange) { _, error -> + .subscribeLatestCache(MangaController::onChapterStatusChange) { _, error -> Timber.e(error) } } @@ -473,9 +476,9 @@ class MangaAllInOnePresenter( /** * Converts a chapter from the database to an extended model, allowing to store new fields. */ - private fun Chapter.toModel(): MangaAllInOneChapterItem { + private fun Chapter.toModel(): ChapterItem { // Create the model object. - val model = MangaAllInOneChapterItem(this, manga) + val model = ChapterItem(this, manga) // Find an active download for this chapter. val download = downloadManager.queue.find { it.chapter.id == id } @@ -492,29 +495,60 @@ class MangaAllInOnePresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(chapters: List) { + private fun setDownloadedChapters(chapters: List) { chapters .filter { downloadManager.isChapterDownloaded(it, manga) } .forEach { it.status = Download.DOWNLOADED } } + /** + * Requests an updated list of chapters from the source. + */ + fun fetchChaptersFromSource(manualFetch: Boolean = false) { + hasRequested = true + + if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return + fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } + .subscribeOn(Schedulers.io()) + .map { syncChaptersWithSource(db, it, manga, source) } + .doOnNext { + if (manualFetch) { + downloadNewChapters(it.first) + } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, _ -> + view.onFetchChaptersDone() + }, + MangaController::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(chapterList: List): List { - var chapters = chapterList + private fun applyChapterFilters(chapters: List): Observable> { + var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) if (onlyUnread()) { - chapters = chapters.filter { !it.read } + observable = observable.filter { !it.read } } else if (onlyRead()) { - chapters = chapters.filter { it.read } + observable = observable.filter { it.read } } if (onlyDownloaded()) { - chapters = chapters.filter { it.isDownloaded || it.manga.source == LocalSource.ID } + observable = observable.filter { it.isDownloaded || it.manga.isLocal() } } if (onlyBookmarked()) { - chapters = chapters.filter { it.bookmark } + observable = observable.filter { it.bookmark } } val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { Manga.SORTING_SOURCE -> when (sortDescending()) { @@ -529,10 +563,9 @@ class MangaAllInOnePresenter( true -> { c1, c2 -> c2.date_upload.compareTo(c1.date_upload) } false -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } } - else -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } + else -> throw NotImplementedError("Unimplemented sorting method") } - chapters = chapters.sortedWith(Comparator(sortFunction)) - return chapters + return observable.toSortedList(sortFunction) } /** @@ -551,14 +584,14 @@ class MangaAllInOnePresenter( // Force UI update if downloaded filter active and download finished. if (onlyDownloaded() && download.status == Download.DOWNLOADED) { - updateChaptersView() + refreshChapters() } } /** * Returns the next unread chapter or null if everything is read. */ - fun getNextUnreadChapter(): MangaAllInOneChapterItem? { + fun getNextUnreadChapter(): ChapterItem? { return chapters.sortedByDescending { it.source_order }.find { !it.read } } @@ -567,21 +600,22 @@ class MangaAllInOnePresenter( * @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 /* --> EH */ && !preferences - .eh_preserveReadingPosition() - .get() /* <-- EH */ - ) { - chapter.last_page_read = 0 - } + fun markChaptersRead(selectedChapters: List, read: Boolean) { + val chapters = selectedChapters.map { chapter -> + chapter.read = read + if (!read) { + chapter.last_page_read = 0 } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() + chapter + } + + launchIO { + db.updateChaptersProgress(chapters).executeAsBlocking() + + if (preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } } /** @@ -596,7 +630,7 @@ class MangaAllInOnePresenter( * Bookmarks the given list of chapters. * @param selectedChapters the list of chapters to bookmark. */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { Observable.from(selectedChapters) .doOnNext { chapter -> chapter.bookmark = bookmarked @@ -611,22 +645,22 @@ class MangaAllInOnePresenter( * Deletes the given list of chapter. * @param chapters the list of chapters to delete. */ - fun deleteChapters(chapters: List) { + fun deleteChapters(chapters: List) { Observable.just(chapters) .doOnNext { deleteChaptersInternal(chapters) } - .doOnNext { if (onlyDownloaded()) updateChaptersView() } + .doOnNext { if (onlyDownloaded()) refreshChapters() } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeFirst( { view, _ -> view.onChaptersDeleted(chapters) }, - MangaAllInOneController::onChaptersDeletedError + MangaController::onChaptersDeletedError ) } private fun downloadNewChapters(chapters: List) { - if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences) /* SY --> */ || manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID/* SY <-- */) return + if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences)) return downloadChapters(chapters) } @@ -635,7 +669,7 @@ class MangaAllInOnePresenter( * Deletes a list of chapters from disk. This method is called in a background thread. * @param chapters the chapters to delete. */ - private fun deleteChaptersInternal(chapters: List) { + private fun deleteChaptersInternal(chapters: List) { downloadManager.deleteChapters(chapters, manga, source) chapters.forEach { it.status = Download.NOT_DOWNLOADED @@ -646,10 +680,10 @@ class MangaAllInOnePresenter( /** * Reverses the sorting and requests an UI update. */ - fun revertSortOrder() { + fun reverseSortOrder() { manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) db.updateFlags(manga).executeAsBlocking() - updateChaptersView() + refreshChapters() } /** @@ -659,7 +693,7 @@ class MangaAllInOnePresenter( fun setUnreadFilter(onlyUnread: Boolean) { manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL db.updateFlags(manga).executeAsBlocking() - updateChaptersView() + refreshChapters() } /** @@ -669,7 +703,7 @@ class MangaAllInOnePresenter( fun setReadFilter(onlyRead: Boolean) { manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL db.updateFlags(manga).executeAsBlocking() - updateChaptersView() + refreshChapters() } /** @@ -679,7 +713,7 @@ class MangaAllInOnePresenter( fun setDownloadedFilter(onlyDownloaded: Boolean) { manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL db.updateFlags(manga).executeAsBlocking() - updateChaptersView() + refreshChapters() } /** @@ -689,7 +723,7 @@ class MangaAllInOnePresenter( fun setBookmarkedFilter(onlyBookmarked: Boolean) { manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL db.updateFlags(manga).executeAsBlocking() - updateChaptersView() + refreshChapters() } /** @@ -700,14 +734,7 @@ class MangaAllInOnePresenter( manga.downloadedFilter = Manga.SHOW_ALL manga.bookmarkedFilter = Manga.SHOW_ALL db.updateFlags(manga).executeAsBlocking() - updateChaptersView() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - setFavorite(true) + refreshChapters() } /** @@ -726,7 +753,7 @@ class MangaAllInOnePresenter( fun setSorting(sort: Int) { manga.sorting = sort db.updateFlags(manga).executeAsBlocking() - updateChaptersView() + refreshChapters() } /** @@ -770,4 +797,6 @@ class MangaAllInOnePresenter( fun sortDescending(): Boolean { return manga.sortDescending() } + + // Chapters list - end } 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 1f2491c56..4f4703e0f 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 @@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneAdapter import eu.kanade.tachiyomi.util.view.visibleIf import java.util.Date import kotlinx.android.synthetic.main.chapters_item.bookmark_icon @@ -77,65 +76,3 @@ class ChapterHolder( } } } - -class MangaAllInOneChapterHolder( - view: View, - private val adapter: MangaAllInOneAdapter -) : BaseFlexibleViewHolder(view, adapter) { - - fun bind(item: MangaAllInOneChapterItem, manga: Manga) { - val chapter = item.chapter - - chapter_title.text = when (manga.displayMode) { - Manga.DISPLAY_NUMBER -> { - val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - itemView.context.getString(R.string.display_mode_chapter, number) - } - else -> chapter.name - } - - // Set correct text color - val chapterColor = when { - chapter.read -> adapter.readColor - chapter.bookmark -> adapter.bookmarkedColor - else -> adapter.unreadColor - } - chapter_title.setTextColor(chapterColor) - chapter_description.setTextColor(chapterColor) - - bookmark_icon.visibleIf { chapter.bookmark } - - val descriptions = mutableListOf() - - if (chapter.date_upload > 0) { - descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload))) - } - if (!chapter.read && chapter.last_page_read > 0) { - val lastPageRead = SpannableString(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)).apply { - setSpan(ForegroundColorSpan(adapter.readColor), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) - } - descriptions.add(lastPageRead) - } - if (!chapter.scanlator.isNullOrBlank()) { - descriptions.add(chapter.scanlator!!) - } - - if (descriptions.isNotEmpty()) { - chapter_description.text = descriptions.joinTo(SpannableStringBuilder(), " • ") - } else { - chapter_description.text = "" - } - - notifyStatus(item.status) - } - - fun notifyStatus(status: Int) = with(download_text) { - when (status) { - Download.QUEUE -> setText(R.string.chapter_queued) - Download.DOWNLOADING -> setText(R.string.chapter_downloading) - Download.DOWNLOADED -> setText(R.string.chapter_downloaded) - Download.ERROR -> setText(R.string.chapter_error) - else -> text = "" - } - } -} 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 53acd7f5d..649cab753 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 @@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneAdapter class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), @@ -58,51 +57,3 @@ class ChapterItem(val chapter: Chapter, val manga: Manga) : return chapter.id!!.hashCode() } } - -class MangaAllInOneChapterItem(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(view: View, adapter: FlexibleAdapter>): MangaAllInOneChapterHolder { - return MangaAllInOneChapterHolder(view, adapter as MangaAllInOneAdapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: MangaAllInOneChapterHolder, - position: Int, - payloads: List? - ) { - holder.bind(this, manga) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is MangaAllInOneChapterItem) { - return chapter.id!! == other.chapter.id!! - } - return false - } - - override fun hashCode(): Int { - return chapter.id!!.hashCode() - } -} 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 e44f25cb9..9ead9cde9 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 @@ -4,6 +4,7 @@ import android.content.Context import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.getResourceColor import java.text.DateFormat import java.text.DecimalFormat @@ -11,7 +12,7 @@ import java.text.DecimalFormatSymbols import uy.kohesive.injekt.injectLazy class ChaptersAdapter( - controller: Any, + controller: MangaController, context: Context ) : FlexibleAdapter(null, controller, true) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt deleted file mode 100644 index 47c45dbfe..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ /dev/null @@ -1,612 +0,0 @@ -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.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.core.graphics.drawable.DrawableCompat -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.databinding.ChaptersControllerBinding -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.getCoordinates -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.shrinkOnScroll -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.visible -import exh.EH_SOURCE_ID -import exh.EXH_SOURCE_ID -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.swiperefreshlayout.refreshes -import timber.log.Timber - -class ChaptersController : - NucleusController(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - DownloadCustomChaptersDialog.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() - - private var lastClickPosition = -1 - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - - override fun createPresenter(): ChaptersPresenter { - val ctrl = parentController as MangaController - return ChaptersPresenter( - ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay - ) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = ChaptersControllerBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - val ctrl = parentController as MangaController - if (ctrl.manga == null || ctrl.source == null) return - - // Init RecyclerView and adapter - adapter = ChaptersAdapter(this, view.context) - - binding.recycler.adapter = adapter - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - binding.recycler.setHasFixedSize(true) - adapter?.fastScroller = binding.fastScroller - - binding.swipeRefresh.refreshes() - .onEach { fetchChaptersFromSource(manualFetch = true) } - .launchIn(scope) - - binding.fab.clicks() - .onEach { - 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 = binding.fab.getCoordinates() - if (!binding.revealView.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else { - view.context.toast(R.string.no_next_chapter) - } - } - .launchIn(scope) - - binding.fab.shrinkOnScroll(binding.recycler) - - binding.actionToolbar.offsetAppbarHeight(activity!!) - binding.fab.offsetAppbarHeight(activity!!) - } - - override fun onDestroyView(view: View) { - destroyActionModeIfNeeded() - binding.actionToolbar.destroy() - adapter = null - super.onDestroyView(view) - } - - override fun onActivityResumed(activity: Activity) { - if (view == null) return - - // Check if animation view is visible - if (binding.revealView.visibility == View.VISIBLE) { - // Show the unreveal effect - val coordinates = binding.fab.getCoordinates() - binding.revealView.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) - val menuFilterEmpty = menu.findItem(R.id.action_filter_empty) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked() - - if (filterSet) { - val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) - DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor) - } - - // Only show remove filter option if there's a filter set. - menuFilterEmpty.isVisible = filterSet - - // Disable unread filter option if read filter is enabled. - if (presenter.onlyRead()) { - menuFilterUnread.isEnabled = false - } - // Disable read filter option if unread filter is enabled. - if (presenter.onlyUnread()) { - menuFilterRead.isEnabled = false - } - - // Display mode submenu - if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { - menu.findItem(R.id.display_title).isChecked = true - } else { - menu.findItem(R.id.display_chapter_number).isChecked = true - } - - // Sorting mode submenu - val sortingItem = when (presenter.manga.sorting) { - Manga.SORTING_SOURCE -> R.id.sort_by_source - Manga.SORTING_NUMBER -> R.id.sort_by_number - Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date - else -> throw NotImplementedError("Unimplemented sorting method") - } - menu.findItem(sortingItem).isChecked = true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.display_title -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NAME) - } - R.id.display_chapter_number -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NUMBER) - } - - R.id.sort_by_source -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_SOURCE) - } - R.id.sort_by_number -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_NUMBER) - } - R.id.sort_by_upload_date -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_UPLOAD_DATE) - } - - R.id.download_next, R.id.download_next_5, R.id.download_next_10, - R.id.download_custom, R.id.download_unread, R.id.download_all - -> downloadChapters(item.itemId) - - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - R.id.action_sort -> presenter.revertSortOrder() - } - return super.onOptionsItemSelected(item) - } - - fun onNextChapters(chapters: List) { - // If the list is empty and it hasn't requested previously, fetch chapters from source - // We use presenter chapters instead because they are always unfiltered - if (!presenter.hasRequested && presenter.chapters.isEmpty()) { - fetchChaptersFromSource() - } - - val mangaController = parentController as MangaController - if (mangaController.update || - // Auto-update old format galleries - ( - (presenter.manga.source == EH_SOURCE_ID || presenter.manga.source == EXH_SOURCE_ID) && - chapters.size == 1 && chapters.first().date_upload == 0L - ) - ) { - mangaController.update = false - fetchChaptersFromSource() - } - - 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() - } - - val context = view?.context - if (context != null && chapters.any { it.read }) { - binding.fab.text = context.getString(R.string.action_resume) - } - } - - private fun fetchChaptersFromSource(manualFetch: Boolean = false) { - binding.swipeRefresh.isRefreshing = true - presenter.fetchChaptersFromSource(manualFetch) - } - - fun onFetchChaptersDone() { - binding.swipeRefresh.isRefreshing = false - } - - fun onFetchChaptersError(error: Throwable) { - binding.swipeRefresh.isRefreshing = false - activity?.toast(error.message) - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder - } - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - override fun onItemClick(view: View?, position: Int): Boolean { - val adapter = adapter ?: return false - val item = adapter.getItem(position) ?: return false - return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - lastClickPosition = position - toggleSelection(position) - true - } else { - openChapter(item.chapter) - false - } - } - - override fun onItemLongClick(position: Int) { - createActionModeIfNeeded() - when { - lastClickPosition == -1 -> setSelection(position) - lastClickPosition > position -> - for (i in position until lastClickPosition) - setSelection(i) - lastClickPosition < position -> - for (i in lastClickPosition + 1..position) - setSelection(i) - else -> setSelection(position) - } - lastClickPosition = position - adapter?.notifyDataSetChanged() - } - - // SELECTIONS & ACTION MODE - - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - adapter.toggleSelection(position) - adapter.notifyDataSetChanged() - if (adapter.isSelected(position)) { - selectedItems.add(item) - } else { - selectedItems.remove(item) - } - actionMode?.invalidate() - } - - private fun setSelection(position: Int) { - val adapter = adapter ?: return - val item = adapter.getItem(position) ?: return - if (!adapter.isSelected(position)) { - adapter.toggleSelection(position) - selectedItems.add(item) - actionMode?.invalidate() - } - } - - private fun getSelectedChapters(): List { - val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } - } - - private fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) - binding.actionToolbar.show( - actionMode!!, - R.menu.chapter_selection - ) { onActionItemClicked(it!!) } - } - } - - private fun destroyActionModeIfNeeded() { - lastClickPosition = -1 - actionMode?.finish() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.generic_selection, menu) - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = adapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = count.toString() - - val isLocalSource = presenter.source.id == LocalSource.ID - val chapters = getSelectedChapters() - binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && chapters.any { !it.isDownloaded } - binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && chapters.any { it.isDownloaded } - binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } - binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } - binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } - binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } - - // Hide FAB to avoid interfering with the bottom action toolbar - // binding.fab.hide() - binding.fab.gone() - } - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return onActionItemClicked(item) - } - - private fun onActionItemClicked(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_select_inverse -> selectInverse() - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> showDeleteChaptersConfirmationDialog() - R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true) - R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false) - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_mark_previous_as_read -> markPreviousAsRead(getSelectedChapters()) - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - binding.actionToolbar.hide() - adapter?.mode = SelectableAdapter.Mode.SINGLE - adapter?.clearSelection() - selectedItems.clear() - actionMode = null - - // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] - // fails to show up properly - // binding.fab.show() - binding.fab.visible() - } - - override fun onDetach(view: View) { - destroyActionModeIfNeeded() - super.onDetach(view) - } - - // SELECTION MODE ACTIONS - - private fun selectAll() { - val adapter = adapter ?: return - adapter.selectAll() - selectedItems.addAll(adapter.items) - actionMode?.invalidate() - } - - private fun selectInverse() { - val adapter = adapter ?: return - - selectedItems.clear() - for (i in 0..adapter.itemCount) { - adapter.toggleSelection(i) - } - selectedItems.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) - - actionMode?.invalidate() - adapter.notifyDataSetChanged() - } - - private fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - destroyActionModeIfNeeded() - } - - private fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - destroyActionModeIfNeeded() - } - - private fun downloadChapters(chapters: List) { - val view = view - presenter.downloadChapters(chapters) - if (view != null && !presenter.manga.favorite) { - binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - } - } - destroyActionModeIfNeeded() - } - - private fun showDeleteChaptersConfirmationDialog() { - DeleteChaptersDialog(this).showDialog(router) - } - - override fun deleteChapters() { - deleteChapters(getSelectedChapters()) - } - - private fun markPreviousAsRead(chapters: List) { - val adapter = adapter ?: return - val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = prevChapters.indexOf(chapters.last()) - if (chapterPos != -1) { - markAsRead(prevChapters.take(chapterPos)) - } - destroyActionModeIfNeeded() - } - - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - presenter.bookmarkChapters(chapters, bookmarked) - destroyActionModeIfNeeded() - } - - fun deleteChapters(chapters: List) { - if (chapters.isEmpty()) return - - presenter.deleteChapters(chapters) - destroyActionModeIfNeeded() - } - - fun onChaptersDeleted(chapters: List) { - // this is needed so the downloaded text gets removed from the item - chapters.forEach { - adapter?.updateItem(it) - } - adapter?.notifyDataSetChanged() - } - - fun onChaptersDeletedError(error: Throwable) { - Timber.e(error) - } - - // OVERFLOW MENU DIALOGS - - private fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - adapter?.notifyDataSetChanged() - } - - private fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - private fun downloadChapters(choice: Int) { - val chaptersToDownload = when (choice) { - R.id.download_next -> getUnreadChaptersSorted().take(1) - R.id.download_next_5 -> getUnreadChaptersSorted().take(5) - R.id.download_next_10 -> getUnreadChaptersSorted().take(10) - R.id.download_custom -> { - showCustomDownloadDialog() - return - } - R.id.download_unread -> presenter.chapters.filter { !it.read } - R.id.download_all -> presenter.chapters - else -> emptyList() - } - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - destroyActionModeIfNeeded() - } - - private fun showCustomDownloadDialog() { - DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router) - } - - override fun downloadCustomChapters(amount: Int) { - val chaptersToDownload = getUnreadChaptersSorted().take(amount) - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt deleted file mode 100755 index 1cd170770..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ /dev/null @@ -1,488 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.os.Bundle -import com.jakewharton.rxrelay.BehaviorRelay -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource -import eu.kanade.tachiyomi.util.isLocal -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.shouldDownloadNewChapters -import exh.EH_SOURCE_ID -import exh.EXH_SOURCE_ID -import exh.debug.DebugToggles -import exh.eh.EHentaiUpdateHelper -import java.util.Date -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy - -class ChaptersPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - val preferences: PreferencesHelper = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get() -) : BasePresenter() { - - /** - * List of chapters of the manga. It's always unfiltered and unsorted. - */ - var chapters: List = emptyList() - private set - - /** - * Subject of list of chapters to allow updating the view without going to DB. - */ - private 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 - - // EXH --> - private val updateHelper: EHentaiUpdateHelper by injectLazy() - - val redirectUserRelay = BehaviorRelay.create() - - data class EXHRedirect(val manga: Manga, val update: Boolean) - // EXH <-- - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Prepare the relay. - chaptersRelay.flatMap { applyChapterFilters(it) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(ChaptersController::onNextChapters) { _, error -> Timber.e(error) } - - // Add the subscription that retrieves the chapters from the database, keeps subscribed to - // changes, and sends the list of chapters to the relay. - add( - db.getChapters(manga).asRxObservable() - .map { chapters -> - // Convert every chapter to a model. - chapters.map { it.toModel() } - } - .doOnNext { chapters -> - // Find downloaded chapters - setDownloadedChapters(chapters) - - // Store the last emission - this.chapters = chapters - - // Listen for download status changes - observeDownloads() - - // Emit the number of chapters to the info tab. - chapterCountRelay.call( - chapters.maxBy { it.chapter_number }?.chapter_number - ?: 0f - ) - - // Emit the upload date of the most recent chapter - lastUpdateRelay.call( - Date( - chapters.maxBy { it.date_upload }?.date_upload - ?: 0 - ) - ) - // EXH --> - if (chapters.isNotEmpty() && - (source.id == EXH_SOURCE_ID || source.id == EH_SOURCE_ID) && - DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled - ) { - // Check for gallery in library and accept manga with lowest id - // Find chapters sharing same root - add( - updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters) - .subscribeOn(Schedulers.io()) - .subscribe { (acceptedChain, _) -> - // Redirect if we are not the accepted root - if (manga.id != acceptedChain.manga.id) { - // Update if any of our chapters are not in accepted manga's chapters - val ourChapterUrls = chapters.map { it.url }.toSet() - val acceptedChapterUrls = acceptedChain.chapters.map { it.url }.toSet() - val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty() - redirectUserRelay.call(EXHRedirect(acceptedChain.manga, update)) - } - } - ) - } - // EXH <-- - } - .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) { - chapters - .filter { downloadManager.isChapterDownloaded(it, manga) } - .forEach { it.status = Download.DOWNLOADED } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource(manualFetch: Boolean = false) { - hasRequested = true - - if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .doOnNext { - if (manualFetch) { - downloadNewChapters(it.first) - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, _ -> - view.onFetchChaptersDone() - }, - ChaptersController::onFetchChaptersError - ) - } - - /** - * Updates the UI after applying the filters. - */ - private fun refreshChapters() { - chaptersRelay.call(chapters) - } - - /** - * Applies the view filters to the list of chapters obtained from the database. - * @param chapters the list of chapters from the database - * @return an observable of the list of chapters filtered and sorted. - */ - private fun applyChapterFilters(chapters: List): Observable> { - var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) - if (onlyUnread()) { - observable = observable.filter { !it.read } - } else if (onlyRead()) { - observable = observable.filter { it.read } - } - if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded || it.manga.isLocal() } - } - 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) } - } - Manga.SORTING_UPLOAD_DATE -> when (sortDescending()) { - true -> { c1, c2 -> c2.date_upload.compareTo(c1.date_upload) } - false -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } - } - 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. - */ - private 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 /* --> EH */ && !preferences - .eh_preserveReadingPosition() - .get() /* <-- EH */ - ) { - chapter.last_page_read = 0 - } - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Downloads the given list of chapters with the manager. - * @param chapters the list of chapters to download. - */ - fun downloadChapters(chapters: List) { - downloadManager.downloadChapters(manga, chapters) - } - - /** - * Bookmarks the given list of chapters. - * @param selectedChapters the list of chapters to bookmark. - */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.bookmark = bookmarked - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Deletes the given list of chapter. - * @param chapters the list of chapters to delete. - */ - fun deleteChapters(chapters: List) { - Observable.just(chapters) - .doOnNext { deleteChaptersInternal(chapters) } - .doOnNext { if (onlyDownloaded()) refreshChapters() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, _ -> - view.onChaptersDeleted(chapters) - }, - ChaptersController::onChaptersDeletedError - ) - } - - private fun downloadNewChapters(chapters: List) { - if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences) /* SY --> */ || manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID/* SY <-- */) return - - downloadChapters(chapters) - } - - /** - * Deletes a list of chapters from disk. This method is called in a background thread. - * @param chapters the chapters to delete. - */ - private fun deleteChaptersInternal(chapters: List) { - downloadManager.deleteChapters(chapters, manga, source) - chapters.forEach { - it.status = Download.NOT_DOWNLOADED - it.download = null - } - } - - /** - * Reverses the sorting and requests an UI update. - */ - fun revertSortOrder() { - manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyUnread whether to display only unread chapters or all chapters. - */ - fun setUnreadFilter(onlyUnread: Boolean) { - manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyRead whether to display only read chapters or all chapters. - */ - fun setReadFilter(onlyRead: Boolean) { - manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the download filter and requests an UI update. - * @param onlyDownloaded whether to display only downloaded chapters or all chapters. - */ - fun setDownloadedFilter(onlyDownloaded: Boolean) { - manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the bookmark filter and requests an UI update. - * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. - */ - fun setBookmarkedFilter(onlyBookmarked: Boolean) { - manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - mangaFavoriteRelay.call(true) - } - - /** - * Sets the active display mode. - * @param mode the mode to set. - */ - fun setDisplayMode(mode: Int) { - manga.displayMode = mode - db.updateFlags(manga).executeAsBlocking() - } - - /** - * Sets the sorting method and requests an UI update. - * @param sort the sorting mode. - */ - fun setSorting(sort: Int) { - manga.sorting = sort - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Whether downloaded only mode is enabled. - */ - fun forceDownloaded(): Boolean { - return manga.favorite && preferences.downloadedOnly().get() - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyDownloaded(): Boolean { - return forceDownloaded() || 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/MangaChaptersHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaChaptersHeaderAdapter.kt new file mode 100644 index 000000000..bb0ad2d39 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaChaptersHeaderAdapter.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.MangaChaptersHeaderBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job + +class MangaChaptersHeaderAdapter : + RecyclerView.Adapter() { + + private var numChapters: Int? = null + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + private lateinit var binding: MangaChaptersHeaderBinding + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { + binding = MangaChaptersHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return HeaderViewHolder(binding.root) + } + + override fun getItemCount(): Int = 1 + + override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) { + holder.bind() + } + + fun setNumChapters(numChapters: Int) { + this.numChapters = numChapters + + notifyDataSetChanged() + } + + inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + fun bind() { + binding.chaptersLabel.text = if (numChapters == null) { + view.context.getString(R.string.chapters) + } else { + view.context.resources.getQuantityString(R.plurals.manga_num_chapters, numChapters!!, numChapters) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt new file mode 100644 index 000000000..f7e5daf1c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.ui.manga.info + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import kotlin.math.min + +/** + * A custom ImageView for holding a manga cover with: + * - width: min(maxWidth attr, 33% of parent width) + * - height: 2:3 width:height ratio + * + * Should be defined with a width of match_parent. + */ +class MangaCoverImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) { + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val width = min(maxWidth, measuredWidth / 3) + val height = width / 2 * 3 + setMeasuredDimension(width, height) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt deleted file mode 100644 index 3222c312a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ /dev/null @@ -1,713 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.google.gson.Gson -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.glide.GlideApp -import eu.kanade.tachiyomi.data.glide.toMangaThumbnail -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.MangaInfoControllerBinding -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.all.MergedSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController -import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.recent.history.HistoryController -import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController -import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.visible -import eu.kanade.tachiyomi.util.view.visibleIf -import exh.EH_SOURCE_ID -import exh.EXH_SOURCE_ID -import exh.MERGED_SOURCE_ID -import exh.util.setChipsExtended -import java.text.DateFormat -import java.text.DecimalFormat -import java.util.Date -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.android.view.longClicks -import reactivecircus.flowbinding.swiperefreshlayout.refreshes -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy - -/** - * Fragment that shows manga information. - * Uses R.layout.manga_info_controller. - * UI related actions should be called from here. - */ -class MangaInfoController(private val fromSource: Boolean = false) : - NucleusController(), - ChangeMangaCategoriesDialog.Listener, - CoroutineScope { - - private val preferences: PreferencesHelper by injectLazy() - - private val dateFormat: DateFormat by lazy { - preferences.dateFormat() - } - - private var initialLoad: Boolean = true - - // EXH --> - private var lastMangaThumbnail: String? = null - - private val smartSearchConfig get() = (parentController as MangaController).smartSearchConfig - - override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main - - private val gson: Gson by injectLazy() - - private val sourceManager: SourceManager by injectLazy() - // EXH <-- - - override fun createPresenter(): MangaInfoPresenter { - val ctrl = parentController as MangaController - return MangaInfoPresenter( - ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay, ctrl.smartSearchConfig - ) - } - - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = MangaInfoControllerBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // For rounded corners - binding.mangaCover.clipToOutline = true - - binding.btnFavorite.clicks() - .onEach { onFavoriteClick() } - .launchIn(scope) - - if (presenter.manga.favorite && presenter.getCategories().isNotEmpty()) { - binding.btnCategories.visible() - } - binding.btnCategories.clicks() - .onEach { onCategoriesClick() } - .launchIn(scope) - - if (presenter.source is HttpSource) { - binding.btnWebview.visible() - binding.btnShare.visible() - - binding.btnWebview.clicks() - .onEach { openInWebView() } - .launchIn(scope) - binding.btnShare.clicks() - .onEach { shareManga() } - .launchIn(scope) - } - - if (presenter.manga.favorite) { - binding.btnMigrate.visible() - binding.btnSmartSearch.visible() - } - - binding.btnMigrate.clicks() - .onEach { - PreMigrationController.navigateToMigration( - preferences.skipPreMigration().get(), - router, - listOf(presenter.manga.id!!) - ) - } - .launchIn(scope) - - binding.btnSmartSearch.clicks() - .onEach { openSmartSearch() } - .launchIn(scope) - - // Set SwipeRefresh to refresh manga data. - binding.swipeRefresh.refreshes() - .onEach { fetchMangaFromSource(manualFetch = true) } - .launchIn(scope) - - binding.mangaFullTitle.longClicks() - .onEach { - activity?.copyToClipboard( - view.context.getString(R.string.title), - binding.mangaFullTitle.text.toString() - ) - } - .launchIn(scope) - - binding.mangaFullTitle.clicks() - .onEach { - performGlobalSearch(binding.mangaFullTitle.text.toString()) - } - .launchIn(scope) - - binding.mangaAuthor.longClicks() - .onEach { - // EXH Special case E-Hentai/ExHentai to ignore author field (unused) - if (!isEHentaiBasedSource()) { - activity?.copyToClipboard( - binding.mangaAuthor.text.toString(), - binding.mangaAuthor.text.toString() - ) - } - } - .launchIn(scope) - - binding.mangaAuthor.clicks() - .onEach { - // EXH Special case E-Hentai/ExHentai to ignore author field (unused) - if (!isEHentaiBasedSource()) { - performGlobalSearch(binding.mangaAuthor.text.toString()) - } - } - .launchIn(scope) - - binding.mangaSummary.longClicks() - .onEach { - activity?.copyToClipboard( - view.context.getString(R.string.description), - binding.mangaSummary.text.toString() - ) - } - .launchIn(scope) - - binding.mangaCover.longClicks() - .onEach { - activity?.copyToClipboard( - view.context.getString(R.string.title), - presenter.manga.title - ) - } - .launchIn(scope) - - // EXH --> - if (smartSearchConfig == null) { - binding.recommendBtn.visible() - binding.recommendBtn.clicks() - .onEach { openRecommends() } - .launchIn(scope) - } - smartSearchConfig?.let { smartSearchConfig -> - if (smartSearchConfig.origMangaId != null) { binding.mergeBtn.visible() } - binding.mergeBtn.clicks() - .onEach { - // Init presenter here to avoid threading issues - presenter - - launch { - try { - val mergedManga = withContext(Dispatchers.IO + NonCancellable) { - presenter.smartSearchMerge(presenter.manga, smartSearchConfig.origMangaId!!) - } - - parentController?.router?.pushController( - MangaController( - mergedManga, - true, - update = true - ).withFadeTransaction() - ) - applicationContext?.toast("Manga merged!") - } catch (e: Exception) { - if (e is CancellationException) throw e - else { - applicationContext?.toast("Failed to merge manga: ${e.message}") - } - } - } - } - .launchIn(scope) - } - // EXH <-- - } - - // EXH --> - private fun openSmartSearch() { - val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.originalTitle, presenter.manga.id!!) - - parentController?.router?.pushController( - SourceController( - Bundle().apply { - putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig) - } - ).withFadeTransaction() - ) - } - // EXH <-- - - // AZ --> - private fun openRecommends() { - val recommendsConfig = BrowseSourceController.RecommendsConfig(presenter.manga) - - parentController?.router?.pushController( - BrowseSourceController( - Bundle().apply { - putParcelable(BrowseSourceController.RECOMMENDS_CONFIG, recommendsConfig) - } - ).withFadeTransaction() - ) - } - // AZ <-- - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextManga(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source) - } else { - // Initialize manga. - fetchMangaFromSource() - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - val view = view ?: return - - // update full title TextView. - binding.mangaFullTitle.text = if (manga.title.isBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.title - } - - // Update author/artist TextView. - val authors = listOf(manga.author, manga.artist).filter { !it.isNullOrBlank() }.distinct() - binding.mangaAuthor.text = if (authors.isEmpty()) { - view.context.getString(R.string.unknown) - } else { - authors.joinToString(", ") - } - - // If manga source is known update source TextView. - val mangaSource = source?.toString() - with(binding.mangaSource) { - // EXH --> - if (mangaSource == null) { - text = view.context.getString(R.string.unknown) - } else if (source.id == MERGED_SOURCE_ID) { - text = MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { - sourceManager.getOrStub(it.source).toString() - }.distinct().joinToString() - } else { - text = mangaSource - setOnClickListener { - val sourceManager = Injekt.get() - performSearch(sourceManager.getOrStub(source.id).name) - } - } - // EXH <-- - } - - // EXH --> - if (source?.id == MERGED_SOURCE_ID) { - binding.mangaSourceLabel.text = "Sources" - } else { - binding.mangaSourceLabel.setText(R.string.manga_info_source_label) - } - // EXH <-- - - // Update status TextView. - binding.mangaStatus.setText( - when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - } - ) - - // Set the favorite drawable to the correct one. - setFavoriteButtonState(manga.favorite) - - // Set cover if it wasn't already. - val mangaThumbnail = manga.toMangaThumbnail() - - GlideApp.with(view.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(binding.mangaCover) - - binding.backdrop?.let { - GlideApp.with(view.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(it) - } - - // Manga info section - if (manga.description.isNullOrBlank() && manga.genre.isNullOrBlank()) { - hideMangaInfo() - } else { - // Update description TextView. - binding.mangaSummary.text = if (manga.description.isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.description - } - - // Update genres list - if (!manga.genre.isNullOrBlank()) { - binding.mangaGenresTagsCompactChips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) - binding.mangaGenresTagsFullChips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) - } else { - binding.mangaGenresTagsWrapper.gone() - } - - // Handle showing more or less info - binding.mangaSummary.clicks() - .onEach { toggleMangaInfo(view.context) } - .launchIn(scope) - binding.mangaInfoToggle.clicks() - .onEach { toggleMangaInfo(view.context) } - .launchIn(scope) - - // Expand manga info if navigated from source listing - if (initialLoad && fromSource) { - toggleMangaInfo(view.context) - initialLoad = false - } - } - } - - private fun hideMangaInfo() { - binding.mangaSummaryLabel.gone() - binding.mangaSummary.gone() - binding.mangaGenresTagsWrapper.gone() - binding.mangaInfoToggle.gone() - } - - private fun toggleMangaInfo(context: Context) { - val isExpanded = - binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse) - - binding.mangaInfoToggle.text = - if (isExpanded) { - context.getString(R.string.manga_info_expand) - } else { - context.getString(R.string.manga_info_collapse) - } - - with(binding.mangaSummary) { - maxLines = - if (isExpanded) { - 3 - } else { - Int.MAX_VALUE - } - - ellipsize = - if (isExpanded) { - TextUtils.TruncateAt.END - } else { - null - } - } - - binding.mangaGenresTagsCompact.visibleIf { isExpanded } - binding.mangaGenresTagsFullChips.visibleIf { !isExpanded } - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Float) { - if (count > 0f) { - binding.mangaChapters.text = DecimalFormat("#.#").format(count) - } else { - binding.mangaChapters.text = resources?.getString(R.string.unknown) - } - } - - fun setLastUpdateDate(date: Date) { - if (date.time != 0L) { - binding.mangaLastUpdate.text = dateFormat.format(date) - } else { - binding.mangaLastUpdate.text = resources?.getString(R.string.unknown) - } - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - private 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() - } - } - } - - binding.btnCategories.visibleIf { isNowFavorite && presenter.getCategories().isNotEmpty() } - if (isNowFavorite) { - binding.btnSmartSearch.visible() - binding.btnMigrate.visible() - } else { - binding.btnSmartSearch.gone() - binding.btnMigrate.gone() - } - } - - private fun openInWebView() { - val source = presenter.source as? HttpSource ?: return - - val url = try { - source.mangaDetailsRequest(presenter.manga).url.toString() - } catch (e: Exception) { - return - } - - val activity = activity ?: return - val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title) - startActivity(intent) - } - - /** - * 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 intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Update favorite button with correct drawable and text. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteButtonState(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - binding.btnFavorite.apply { - icon = ContextCompat.getDrawable( - context, - if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp - ) - text = - context.getString(if (isFavorite) R.string.in_library else R.string.add_to_library) - isChecked = isFavorite - } - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaFromSource(manualFetch: Boolean = false) { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource(manualFetch) - } - - /** - * Update swipe refresh to stop showing refresh in progress spinner. - */ - fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - fun onFetchMangaError(error: Throwable) { - setRefreshing(false) - activity?.toast(error.message) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - private fun setRefreshing(value: Boolean) { - binding.swipeRefresh.isRefreshing = value - } - - private fun onFavoriteClick() { - val manga = presenter.manga - - if (manga.favorite) { - toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_removed_library)) - } else { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - - when { - // Default category set - defaultCategory != null -> { - toggleFavorite() - presenter.moveMangaToCategory(manga, defaultCategory) - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - // Automatic 'Default' or no categories - defaultCategoryId == 0 || categories.isEmpty() -> { - toggleFavorite() - presenter.moveMangaToCategory(manga, null) - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - // Choose a category - else -> { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - } - } - - private fun onCategoriesClick() { - val manga = presenter.manga - val categories = presenter.getCategories() - - 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 - - if (!manga.favorite) { - toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - presenter.moveMangaToCategories(manga, categories) - } - - /** - * Perform a global search using the provided query. - * - * @param query the search query to pass to the search controller - */ - private fun performGlobalSearch(query: String) { - val router = parentController?.router ?: return - router.pushController(GlobalSearchController(query).withFadeTransaction()) - } - - // --> EH - private fun wrapTag(namespace: String, tag: String) = - if (tag.contains(' ')) { - "$namespace:\"$tag$\"" - } else { - "$namespace:$tag$" - } - - private fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim() - - private fun isEHentaiBasedSource(): Boolean { - val sourceId = presenter.source.id - return sourceId == EH_SOURCE_ID || - sourceId == EXH_SOURCE_ID - } - // <-- EH - - /** - * Perform a search using the provided query. - * - * @param query the search query to the parent controller - */ - private fun performSearch(query: String) { - val router = parentController?.router ?: return - - if (router.backstackSize < 2) { - return - } - - when (val previousController = router.backstack[router.backstackSize - 2].controller()) { - is LibraryController -> { - router.handleBack() - previousController.search(query) - } - is UpdatesController, - is HistoryController -> { - // Manually navigate to LibraryController - router.handleBack() - (router.activity as MainActivity).setSelectedNavItem(R.id.nav_library) - val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController - controller.search(query) - } - is BrowseSourceController -> { - router.handleBack() - previousController.searchWithQuery(query) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt new file mode 100644 index 000000000..312e51d2e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -0,0 +1,397 @@ +package eu.kanade.tachiyomi.ui.manga.info + +import android.content.Context +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.data.glide.MangaThumbnail +import eu.kanade.tachiyomi.data.glide.toMangaThumbnail +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.all.MergedSource +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.system.copyToClipboard +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.setChips +import eu.kanade.tachiyomi.util.view.setTooltip +import eu.kanade.tachiyomi.util.view.visible +import eu.kanade.tachiyomi.util.view.visibleIf +import exh.MERGED_SOURCE_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks +import reactivecircus.flowbinding.android.view.longClicks +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaInfoHeaderAdapter( + private val controller: MangaController, + private val fromSource: Boolean +) : + RecyclerView.Adapter() { + + private var manga: Manga = controller.presenter.manga + private var source: Source = controller.presenter.source + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + private lateinit var binding: MangaInfoHeaderBinding + + private var initialLoad: Boolean = true + private var currentMangaThumbnail: MangaThumbnail? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { + binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return HeaderViewHolder(binding.root) + } + + override fun getItemCount(): Int = 1 + + override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) { + holder.bind() + } + + /** + * Update the view with manga information. + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun update(manga: Manga, source: Source) { + this.manga = manga + this.source = source + + notifyDataSetChanged() + } + + inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + fun bind() { + // For rounded corners + binding.mangaCover.clipToOutline = true + + binding.btnFavorite.clicks() + .onEach { controller.onFavoriteClick() } + .launchIn(scope) + + if (controller.presenter.manga.favorite && Injekt.get().hasLoggedServices()) { + binding.btnTracking.visible() + binding.btnTracking.clicks() + .onEach { controller.onTrackingClick() } + .launchIn(scope) + } else { + binding.btnTracking.gone() + } + + if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) { + binding.btnCategories.visible() + binding.btnCategories.clicks() + .onEach { controller.onCategoriesClick() } + .launchIn(scope) + binding.btnCategories.setTooltip(R.string.action_move_category) + } else { + binding.btnCategories.gone() + } + + if (controller.presenter.source is HttpSource) { + binding.btnWebview.visible() + binding.btnWebview.clicks() + .onEach { controller.openMangaInWebView() } + .launchIn(scope) + binding.btnWebview.setTooltip(R.string.action_open_in_web_view) + + binding.btnShare.visible() + binding.btnShare.clicks() + .onEach { controller.shareManga() } + .launchIn(scope) + binding.btnShare.setTooltip(R.string.action_share) + } + + // SY --> + if (controller.presenter.manga.favorite) { + binding.btnMigrate.visible() + binding.btnMigrate.clicks() + .onEach { controller.migrateManga() } + .launchIn(scope) + binding.btnMigrate.setTooltip(R.string.migrate) + + binding.btnSmartSearch.visible() + binding.btnSmartSearch.clicks() + .onEach { controller.openSmartSearch() } + .launchIn(scope) + binding.btnSmartSearch.setTooltip(R.string.eh_merge_with_another_source) + } + // SY <-- + + binding.mangaFullTitle.longClicks() + .onEach { + controller.activity?.copyToClipboard( + view.context.getString(R.string.title), + binding.mangaFullTitle.text.toString() + ) + } + .launchIn(scope) + + binding.mangaFullTitle.clicks() + .onEach { + controller.performGlobalSearch(binding.mangaFullTitle.text.toString()) + } + .launchIn(scope) + + binding.mangaAuthor.longClicks() + .onEach { + controller.activity?.copyToClipboard( + binding.mangaAuthor.text.toString(), + binding.mangaAuthor.text.toString() + ) + } + .launchIn(scope) + + binding.mangaAuthor.clicks() + .onEach { + controller.performGlobalSearch(binding.mangaAuthor.text.toString()) + } + .launchIn(scope) + + binding.mangaArtist.longClicks() + .onEach { + controller.activity?.copyToClipboard( + binding.mangaArtist.text.toString(), + binding.mangaArtist.text.toString() + ) + } + .launchIn(scope) + + binding.mangaArtist.clicks() + .onEach { + controller.performGlobalSearch(binding.mangaArtist.text.toString()) + } + .launchIn(scope) + + binding.mangaSummary.longClicks() + .onEach { + controller.activity?.copyToClipboard( + view.context.getString(R.string.description), + binding.mangaSummary.text.toString() + ) + } + .launchIn(scope) + + binding.mangaCover.longClicks() + .onEach { + controller.activity?.copyToClipboard( + view.context.getString(R.string.title), + controller.presenter.manga.title + ) + } + .launchIn(scope) + + // EXH --> + if (controller.smartSearchConfig == null) { + binding.recommendBtn.visible() + binding.recommendBtn.clicks() + .onEach { controller.openRecommends() } + .launchIn(scope) + } else { + if (controller.smartSearchConfig.origMangaId != null) { binding.mergeBtn.visible() } + binding.mergeBtn.clicks() + .onEach { + controller.mergeWithAnother() + } + + .launchIn(scope) + } + // EXH <-- + + setMangaInfo(manga, source) + } + + /** + * 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 full title TextView. + binding.mangaFullTitle.text = if (manga.title.isBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.title + } + + // Update author TextView. + binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) { + view.context.getString(R.string.unknown_author) + } else { + manga.author + } + + // Update artist TextView. + val hasArtist = !manga.artist.isNullOrBlank() && manga.artist != manga.author + binding.mangaArtist.isVisible = hasArtist + if (hasArtist) { + binding.mangaArtist.text = manga.artist + } + + // If manga source is known update source TextView. + val mangaSource = source?.toString() + with(binding.mangaSource) { + // SY --> + if (source != null && source.id == MERGED_SOURCE_ID) { + text = MergedSource.MangaConfig.readFromUrl(Injekt.get(), manga.url).children.map { + Injekt.get().getOrStub(it.source).toString() + }.distinct().joinToString() + } else /* SY <-- */ if (mangaSource != null) { + text = mangaSource + setOnClickListener { + val sourceManager = Injekt.get() + controller.performSearch(sourceManager.getOrStub(source.id).name) + } + } else { + text = view.context.getString(R.string.unknown) + } + } + + // Update status TextView. + binding.mangaStatus.setText( + when (manga.status) { + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed + else -> R.string.unknown_status + } + ) + + // Set the favorite drawable to the correct one. + setFavoriteButtonState(manga.favorite) + + // Set cover if changed. + val mangaThumbnail = manga.toMangaThumbnail() + if (mangaThumbnail != currentMangaThumbnail) { + currentMangaThumbnail = mangaThumbnail + listOf(binding.mangaCover, binding.backdrop) + .forEach { + GlideApp.with(view.context) + .load(mangaThumbnail) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(it) + } + } + + // Manga info section + val hasInfoContent = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank() + showMangaInfo(hasInfoContent) + if (hasInfoContent) { + // Update description TextView. + binding.mangaSummary.text = if (manga.description.isNullOrBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.description + } + + // Update genres list + if (!manga.genre.isNullOrBlank()) { + binding.mangaGenresTagsCompactChips.setChips(manga.getGenres(), controller::performSearch) + binding.mangaGenresTagsFullChips.setChips(manga.getGenres(), controller::performSearch) + } else { + binding.mangaGenresTagsWrapper.gone() + } + + // Handle showing more or less info + binding.mangaSummary.clicks() + .onEach { toggleMangaInfo(view.context) } + .launchIn(scope) + binding.mangaInfoToggle.clicks() + .onEach { toggleMangaInfo(view.context) } + .launchIn(scope) + + // Expand manga info if navigated from source listing + if (initialLoad && fromSource) { + toggleMangaInfo(view.context) + initialLoad = false + } + } + + binding.btnCategories.visibleIf { manga.favorite && controller.presenter.getCategories().isNotEmpty() } + } + + private fun showMangaInfo(visible: Boolean) { + binding.mangaSummaryLabel.visibleIf { visible } + binding.mangaSummary.visibleIf { visible } + binding.mangaGenresTagsWrapper.visibleIf { visible } + binding.mangaInfoToggle.visibleIf { visible } + } + + private fun toggleMangaInfo(context: Context) { + val isExpanded = + binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse) + + with(binding.mangaInfoToggle) { + text = if (isExpanded) { + context.getString(R.string.manga_info_expand) + } else { + context.getString(R.string.manga_info_collapse) + } + + icon = if (isExpanded) { + context.getDrawable(R.drawable.ic_baseline_expand_more_24dp) + } else { + context.getDrawable(R.drawable.ic_baseline_expand_less_24dp) + } + } + + with(binding.mangaSummary) { + maxLines = + if (isExpanded) { + 2 + } else { + Int.MAX_VALUE + } + + ellipsize = + if (isExpanded) { + TextUtils.TruncateAt.END + } else { + null + } + } + + binding.mangaGenresTagsCompact.visibleIf { isExpanded } + binding.mangaGenresTagsFullChips.visibleIf { !isExpanded } + } + + /** + * Update favorite button with correct drawable and text. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun setFavoriteButtonState(isFavorite: Boolean) { + // Set the Favorite drawable to the correct one. + // Border drawable if false, filled drawable if true. + binding.btnFavorite.apply { + icon = ContextCompat.getDrawable( + context, + if (isFavorite) R.drawable.ic_favorite_24dp else R.drawable.ic_favorite_border_24dp + ) + text = + context.getString(if (isFavorite) R.string.in_library else R.string.add_to_library) + isChecked = isFavorite + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt deleted file mode 100755 index b17f69a8b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ /dev/null @@ -1,244 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.os.Bundle -import com.google.gson.Gson -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.source.Source -import eu.kanade.tachiyomi.source.online.all.MergedSource -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.browse.source.SourceController -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.prepUpdateCover -import eu.kanade.tachiyomi.util.removeCovers -import exh.MERGED_SOURCE_ID -import exh.util.await -import java.util.Date -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.withContext -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 - -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -class MangaInfoPresenter( - val manga: Manga, - val source: Source, - private val chapterCountRelay: BehaviorRelay, - private val lastUpdateRelay: BehaviorRelay, - private val mangaFavoriteRelay: PublishRelay, - val smartSearchConfig: SourceController.SmartSearchConfig?, - private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get(), - private val coverCache: CoverCache = Injekt.get(), - private val gson: Gson = Injekt.get() -) : BasePresenter() { - - /** - * Subscription to update the manga from the source. - */ - private var fetchMangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - getMangaObservable() - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) - - // Update chapter count - chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setChapterCount) - - // Update favorite status - mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribe { setFavorite(it) } - .apply { add(this) } - - // update last update date - lastUpdateRelay.observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoController::setLastUpdateDate) - } - - private fun getMangaObservable(): Observable { - return db.getManga(manga.url, manga.source).asRxObservable() - .observeOn(AndroidSchedulers.mainThread()) - } - - /** - * Fetch manga information from source. - */ - fun fetchMangaFromSource(manualFetch: Boolean = false) { - if (!fetchMangaSubscription.isNullOrUnsubscribed()) return - fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } - .map { networkManga -> - manga.prepUpdateCover(coverCache, networkManga, manualFetch) - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).executeAsBlocking() - manga - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, _ -> - view.onFetchMangaDone() - }, - MangaInfoController::onFetchMangaError - ) - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - * - * @return the new status of the manga. - */ - fun toggleFavorite(): Boolean { - manga.favorite = !manga.favorite - if (!manga.favorite) { - manga.removeCovers(coverCache) - } - db.insertManga(manga).executeAsBlocking() - return manga.favorite - } - - private fun setFavorite(favorite: Boolean) { - if (manga.favorite == favorite) { - return - } - toggleFavorite() - } - - /** - * Returns true if the manga has any downloads. - */ - fun hasDownloads(): Boolean { - return downloadManager.getDownloadCount(manga) > 0 - } - - /** - * Deletes all the downloads for the manga. - */ - fun deleteDownloads() { - downloadManager.deleteManga(manga, source) - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - fun getCategories(): List { - return db.getCategories().executeAsBlocking() - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param manga the manga to move. - * @param categories the selected categories. - */ - fun moveMangaToCategories(manga: Manga, categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param manga the manga to move. - * @param category the selected category, or null for default category. - */ - fun moveMangaToCategory(manga: Manga, category: Category?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - /* - suspend fun recommendationView(manga: Manga): Manga { - val title = manga.title - val source = manga.source - - }*/ - suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga { - val originalManga = db.getManga(originalMangaId).await() - ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId") - val toInsert = if (originalManga.source == MERGED_SOURCE_ID) { - originalManga.apply { - val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children - if (originalChildren.any { it.source == manga.source && it.url == manga.url }) { - throw IllegalArgumentException("This manga is already merged with the current manga!") - } - - url = MergedSource.MangaConfig( - originalChildren + MergedSource.MangaSource( - manga.source, - manga.url - ) - ).writeAsUrl(gson) - } - } else { - val newMangaConfig = MergedSource.MangaConfig( - listOf( - MergedSource.MangaSource( - originalManga.source, - originalManga.url - ), - MergedSource.MangaSource( - manga.source, - manga.url - ) - ) - ) - Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.originalTitle, MERGED_SOURCE_ID).apply { - copyFrom(originalManga) - favorite = true - last_update = originalManga.last_update - viewer = originalManga.viewer - chapter_flags = originalManga.chapter_flags - sorting = Manga.SORTING_NUMBER - } - } - - // Note that if the manga are merged in a different order, this won't trigger, but I don't care lol - val existingManga = db.getManga(toInsert.url, toInsert.source).await() - if (existingManga != null) { - withContext(NonCancellable) { - if (toInsert.id != null) { - db.deleteManga(toInsert).await() - } - } - - return existingManga - } - - // Reload chapters immediately - toInsert.initialized = false - - val newId = db.insertManga(toInsert).await().insertedId() - if (newId != null) toInsert.id = newId - - return toInsert - } -} 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 index 942d87d46..4597df4f4 100644 --- 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 @@ -2,31 +2,51 @@ package eu.kanade.tachiyomi.ui.manga.track import android.content.Intent import android.net.Uri +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager +import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.databinding.TrackControllerBinding import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController -import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.swiperefreshlayout.refreshes import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -class TrackController(val fromAllInOne: Boolean = false, val manga: Manga? = null) : - NucleusController(), +class TrackController : + NucleusController, TrackAdapter.OnClickListener, SetTrackStatusDialog.Listener, SetTrackChaptersDialog.Listener, SetTrackScoreDialog.Listener, SetTrackReadingDatesDialog.Listener { + constructor(manga: Manga?) : super( + Bundle().apply { + putLong(MANGA_EXTRA, manga?.id ?: 0) + } + ) { + this.manga = manga + } + + 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 + private var adapter: TrackAdapter? = null init { @@ -35,16 +55,12 @@ class TrackController(val fromAllInOne: Boolean = false, val manga: Manga? = nul setHasOptionsMenu(true) } + override fun getTitle(): String? { + return manga?.title + } + override fun createPresenter(): TrackPresenter { - // SY --> - return ( - if (fromAllInOne && manga != null) { - TrackPresenter(manga) - } else { - TrackPresenter((parentController as MangaController).manga!!) - } - ) - // SY <-- + return TrackPresenter(manga!!) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -55,6 +71,8 @@ class TrackController(val fromAllInOne: Boolean = false, val manga: Manga? = nul override fun onViewCreated(view: View) { super.onViewCreated(view) + if (manga == null) return + adapter = TrackAdapter(this) binding.trackRecycler.layoutManager = LinearLayoutManager(view.context) binding.trackRecycler.adapter = adapter @@ -73,13 +91,6 @@ class TrackController(val fromAllInOne: Boolean = false, val manga: Manga? = nul val atLeastOneLink = trackings.any { it.track != null } adapter?.items = trackings binding.swipeRefresh.isEnabled = atLeastOneLink - // SY --> - if (!fromAllInOne) { - (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) - } else { - (parentController as? MangaAllInOneController)?.setTrackingIcon(atLeastOneLink) - } - // SY <-- } fun onSearchResults(results: List) { @@ -183,6 +194,7 @@ class TrackController(val fromAllInOne: Boolean = false, val manga: Manga? = nul } private companion object { + const val MANGA_EXTRA = "manga" const val TAG_SEARCH_CONTROLLER = "track_search_controller" } } 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 d98665fb0..c7a37982f 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 @@ -36,7 +36,7 @@ class TrackSearchAdapter(context: Context) : } else { holder = v.tag as TrackSearchHolder } - holder.onSetValues(track!!) + holder.onSetValues(track) return v } 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 f7b527772..6a9f8ad8e 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 @@ -84,7 +84,7 @@ class TrackSearchDialog : DialogController { // Do an initial search based on the manga's title if (savedState == null) { - val title = trackController.presenter.manga.originalTitle + val title = trackController.presenter.manga.title view.track_search.append(title) search(title) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt index cf12da3c7..b0c5deb55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt @@ -68,7 +68,7 @@ class AboutController : SettingsController() { } } preference { - titleRes = R.string.changelog + titleRes = R.string.whats_new onClick { // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt index 30e568b97..720171e4e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt @@ -24,6 +24,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { private set var currentChapter: ReaderChapter? = null + /** * Updates this adapter with the given [chapters]. It handles setting a few pages of the * next/previous chapter to allow seamless transitions and inverting the pages if the viewer diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index 17976e204..f12066662 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt @@ -13,14 +13,12 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupRestoreService import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.HistoryControllerBinding import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.system.toast @@ -28,7 +26,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.appcompat.queryTextChanges -import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get /** @@ -163,11 +160,7 @@ class HistoryController : override fun onItemClick(position: Int) { val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return - if (Injekt.get().eh_useNewMangaInterface().get()) { - router.pushController(MangaAllInOneController(manga).withFadeTransaction()) - } else { - router.pushController(MangaController(manga).withFadeTransaction()) - } + router.pushController(MangaController(manga).withFadeTransaction()) } override fun removeHistory(manga: Manga, history: History, all: Boolean) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt index a5566d3d9..0b113f134 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt @@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.NucleusController @@ -25,7 +24,6 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.system.notificationManager @@ -35,8 +33,6 @@ import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.recyclerview.scrollStateChanges import reactivecircus.flowbinding.swiperefreshlayout.refreshes import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get /** * Fragment that shows recent chapters. @@ -287,13 +283,7 @@ class UpdatesController : } private fun openManga(chapter: UpdatesItem) { - // SY --> - if (Injekt.get().eh_useNewMangaInterface().get()) { - router.pushController(MangaAllInOneController(chapter.manga).withFadeTransaction()) - } else { - router.pushController(MangaController(chapter.manga).withFadeTransaction()) - } - // SY <-- + router.pushController(MangaController(chapter.manga).withFadeTransaction()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 2f757dcb3..f99d8ac8d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -110,7 +110,7 @@ class SettingsAdvancedController : SettingsController() { } preferenceCategory { - titleRes = R.string.label_data + titleRes = R.string.label_network preference { titleRes = R.string.pref_clear_cookies diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt index 5e7d859ea..c6caab60f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -267,13 +267,6 @@ class SettingsGeneralController : SettingsController() { "Use HIGHLY EXPERIMENTAL automatic ReCAPTCHA solver. Will be grayed out if unsupported by your device." defaultValue = false } - - switchPreference { - key = Keys.eh_use_new_manga_interface - title = "Use New Manga Interface" - summary = "Use new all in one manga interface" - defaultValue = true - } } // <-- EXH } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt index 584a817a2..d00280075 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt @@ -72,6 +72,11 @@ class SettingsLibraryController : SettingsController() { } .launchIn(scope) } + switchPreference { + key = Keys.jumpToChapters + titleRes = R.string.pref_jump_to_chapters + defaultValue = false + } } preferenceCategory { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt index 9aa3944f6..c5671ea16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt @@ -77,7 +77,7 @@ object ChapterRecognition { } // Remove manga title from chapter title. - val nameWithoutManga = name.replace(manga.originalTitle.toLowerCase(), "").trim() + val nameWithoutManga = name.replace(/* SY --> */ manga.originalTitle.toLowerCase()/* SY <-- */, "").trim() // Check if first value is number after title remove. if (updateChapter(withoutManga.find(nameWithoutManga), chapter)) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 09a4cbbc2..c6da8f3ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -160,7 +160,7 @@ fun syncChaptersWithSource( // Set manga's last update time to latest chapter's fetch time if possible val newestChapter = db.getChapters(manga).executeAsBlocking().maxBy { it.date_fetch } - manga.last_update = newestChapter?.date_fetch ?: manga.last_update + manga.last_update = newestChapter?.date_fetch ?: Date().time db.updateLastUpdated(manga).executeAsBlocking() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 315b1f58d..0f2bf564a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -8,7 +8,9 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.annotation.MenuRes +import androidx.annotation.StringRes import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup @@ -38,6 +40,15 @@ inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Sn return snack } +/** + * Adds a tooltip shown on long press. + * + * @param stringRes String resource for tooltip. + */ +inline fun View.setTooltip(@StringRes stringRes: Int) { + TooltipCompat.setTooltipText(this, context.getString(stringRes)) +} + /** * Shows a popup menu on top of this view. * diff --git a/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt b/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt index 9350195f5..b33a8dbe8 100644 --- a/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt +++ b/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.EhSmartSearchBinding import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.SourceManager @@ -12,7 +11,6 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.CoroutineScope @@ -21,7 +19,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -61,11 +58,7 @@ class SmartSearchController(bundle: Bundle? = null) : NucleusController().eh_useNewMangaInterface().get()) { - MangaAllInOneController(event.manga, true, smartSearchConfig).withFadeTransaction() - } else { - MangaController(event.manga, true, smartSearchConfig).withFadeTransaction() - } + val transaction = MangaController(event.manga, true, smartSearchConfig).withFadeTransaction() withContext(Dispatchers.Main) { router.replaceTopController(transaction) } diff --git a/app/src/main/res/drawable/ic_sort_24dp.xml b/app/src/main/res/drawable/ic_sort_24dp.xml new file mode 100644 index 000000000..28b2c9ecb --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort_by_numeric_white_24dp.xml b/app/src/main/res/drawable/ic_sort_by_numeric_white_24dp.xml deleted file mode 100755 index 5bddea0f5..000000000 --- a/app/src/main/res/drawable/ic_sort_by_numeric_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/manga_info_gradient.xml b/app/src/main/res/drawable/manga_info_gradient.xml new file mode 100644 index 000000000..003e925ea --- /dev/null +++ b/app/src/main/res/drawable/manga_info_gradient.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/layout-land/manga_all_in_one_controller.xml b/app/src/main/res/layout-land/manga_all_in_one_controller.xml deleted file mode 100644 index 5f2a4fcba..000000000 --- a/app/src/main/res/layout-land/manga_all_in_one_controller.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/manga_all_in_one_header.xml b/app/src/main/res/layout-land/manga_all_in_one_header.xml deleted file mode 100644 index 0dfc9df0f..000000000 --- a/app/src/main/res/layout-land/manga_all_in_one_header.xml +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -