Use Tachi previews info + chapters manga page, plus of course SY features integrated into it
Add missed invert tap settings Add missed extension open in settings overflow menu option Cleanup
This commit is contained in:
@@ -67,6 +67,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
GlobalScope.launch { get<DownloadManager>() }
|
||||
|
||||
// SY -->
|
||||
GlobalScope.launch { get<CustomMangaManager>() }
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -52,11 +52,13 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <--
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ sealed class Filter<T>(val name: String, var state: T) {
|
||||
data class Selection(val index: Int, val ascending: Boolean)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
abstract class AutoComplete(name: String, val hint: String, val values: List<String>, val skipAutoFillTags: List<String> = emptyList(), val excludePrefix: String? = null, state: List<String>) : Filter<List<String>>(name, state)
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+11
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-4
@@ -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
|
||||
}
|
||||
|
||||
+6
-18
@@ -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<PreferencesHelper>().eh_useNewMangaInterface().get()) {
|
||||
adapter.controller.router.pushController(
|
||||
MangaAllInOneController(
|
||||
manga,
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
} else {
|
||||
adapter.controller.router.pushController(
|
||||
MangaController(
|
||||
manga,
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
adapter.controller.router.pushController(
|
||||
MangaController(
|
||||
manga,
|
||||
true
|
||||
).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+7
-19
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
+1
-9
@@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<MainActivityBinding>() {
|
||||
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) {
|
||||
|
||||
@@ -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!!)
|
||||
|
||||
@@ -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<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val delegate: MangaAllInOneInterface = controller
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
var items: List<MangaAllInOneChapterItem> = 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<IFlexible<*>>?) {
|
||||
this.items = items as List<MangaAllInOneChapterItem>? ?: 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<MangaAllInOneChapterItem>, lastUpdateDate: Date, chapterCount: Float)
|
||||
fun setMangaInfo(manga: Manga, source: Source?, chapters: List<MangaAllInOneChapterItem>, 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<Manga>, categories: List<Category>)
|
||||
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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<MangaAllInOneHolder>() {
|
||||
|
||||
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<IFlexible<RecyclerView.ViewHolder>>): MangaAllInOneHolder {
|
||||
return MangaAllInOneHolder(view, adapter as MangaAllInOneAdapter, smartSearchConfig)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: MangaAllInOneHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any?>?
|
||||
) {
|
||||
holder.bind(manga, source)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (this === other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return -(manga.id).hashCode()
|
||||
}
|
||||
}
|
||||
@@ -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<TrackManager>().hasLoggedServices())) {
|
||||
binding.btnTracking.visible()
|
||||
}
|
||||
|
||||
setTrackingIcon(
|
||||
Injekt.get<DatabaseHelper>().getTracks(presenter.manga).executeAsBlocking().any {
|
||||
val status = Injekt.get<TrackManager>().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<String> = 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<SourceManager>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+331
-302
@@ -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<MangaAllInOneController>() {
|
||||
// SY -->
|
||||
private val gson: Gson = Injekt.get()
|
||||
// SY <--
|
||||
) : BasePresenter<MangaController>() {
|
||||
|
||||
/**
|
||||
* 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<MangaAllInOneChapterItem> = emptyList()
|
||||
var chapters: List<ChapterItem> = 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<List<ChapterItem>> by lazy {
|
||||
PublishRelay.create<List<ChapterItem>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ChaptersPresenter.EXHRedirect>()
|
||||
// EXH <--
|
||||
private val redirectUserRelay = BehaviorRelay.create<EXHRedirect>()
|
||||
|
||||
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<Manga> {
|
||||
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<MangaAllInOneChapterItem> = 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<SChapter> = 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<Category> {
|
||||
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<Int> {
|
||||
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<Category>) {
|
||||
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<Category> {
|
||||
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<Int> {
|
||||
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<Category>) {
|
||||
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<MangaAllInOneChapterItem>) {
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||
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<MangaAllInOneChapterItem>): List<MangaAllInOneChapterItem> {
|
||||
var chapters = chapterList
|
||||
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
||||
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<MangaAllInOneChapterItem>, 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<ChapterItem>, 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<MangaAllInOneChapterItem>, bookmarked: Boolean) {
|
||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, 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<MangaAllInOneChapterItem>) {
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
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<Chapter>) {
|
||||
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<MangaAllInOneChapterItem>) {
|
||||
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
||||
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
|
||||
}
|
||||
@@ -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<CharSequence>()
|
||||
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChapterHolder>(),
|
||||
@@ -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<MangaAllInOneChapterHolder>(),
|
||||
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<IFlexible<RecyclerView.ViewHolder>>): MangaAllInOneChapterHolder {
|
||||
return MangaAllInOneChapterHolder(view, adapter as MangaAllInOneAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: MangaAllInOneChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChapterItem>(null, controller, true) {
|
||||
|
||||
|
||||
@@ -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<ChaptersControllerBinding, ChaptersPresenter>(),
|
||||
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<ChapterItem>()
|
||||
|
||||
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<ChapterItem>) {
|
||||
// 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<ChapterItem> {
|
||||
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<ChapterItem>) {
|
||||
presenter.markChaptersRead(chapters, true)
|
||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(chapters)
|
||||
}
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun markAsUnread(chapters: List<ChapterItem>) {
|
||||
presenter.markChaptersRead(chapters, false)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
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<ChapterItem>) {
|
||||
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<ChapterItem>, bookmarked: Boolean) {
|
||||
presenter.bookmarkChapters(chapters, bookmarked)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
if (chapters.isEmpty()) return
|
||||
|
||||
presenter.deleteChapters(chapters)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
fun onChaptersDeleted(chapters: List<ChapterItem>) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Float>,
|
||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<ChaptersController>() {
|
||||
|
||||
/**
|
||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||
*/
|
||||
var chapters: List<ChapterItem> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subject of list of chapters to allow updating the view without going to DB.
|
||||
*/
|
||||
private val chaptersRelay: PublishRelay<List<ChapterItem>> by lazy {
|
||||
PublishRelay.create<List<ChapterItem>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<EXHRedirect>()
|
||||
|
||||
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<ChapterItem>) {
|
||||
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<ChapterItem>): Observable<List<ChapterItem>> {
|
||||
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<ChapterItem>, 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<Chapter>) {
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param selectedChapters the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, 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<ChapterItem>) {
|
||||
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<Chapter>) {
|
||||
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<ChapterItem>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<MangaChaptersHeaderAdapter.HeaderViewHolder>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<MangaInfoControllerBinding, MangaInfoPresenter>(),
|
||||
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<SourceManager>()
|
||||
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<Manga>, categories: List<Category>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
||||
|
||||
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<TrackManager>().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<SourceManager>().getOrStub(it.source).toString()
|
||||
}.distinct().joinToString()
|
||||
} else /* SY <-- */ if (mangaSource != null) {
|
||||
text = mangaSource
|
||||
setOnClickListener {
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Float>,
|
||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
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<MangaInfoController>() {
|
||||
|
||||
/**
|
||||
* 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<Manga> {
|
||||
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<Category> {
|
||||
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<Int> {
|
||||
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<Category>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<TrackControllerBinding, TrackPresenter>(),
|
||||
class TrackController :
|
||||
NucleusController<TrackControllerBinding, TrackPresenter>,
|
||||
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<DatabaseHelper>().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<TrackSearch>) {
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class TrackSearchAdapter(context: Context) :
|
||||
} else {
|
||||
holder = v.tag as TrackSearchHolder
|
||||
}
|
||||
holder.onSetValues(track!!)
|
||||
holder.onSetValues(track)
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class AboutController : SettingsController() {
|
||||
}
|
||||
}
|
||||
preference {
|
||||
titleRes = R.string.changelog
|
||||
titleRes = R.string.whats_new
|
||||
|
||||
onClick {
|
||||
// SY -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PreferencesHelper>().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) {
|
||||
|
||||
@@ -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<PreferencesHelper>().eh_useNewMangaInterface().get()) {
|
||||
router.pushController(MangaAllInOneController(chapter.manga).withFadeTransaction())
|
||||
} else {
|
||||
router.pushController(MangaController(chapter.manga).withFadeTransaction())
|
||||
}
|
||||
// SY <--
|
||||
router.pushController(MangaController(chapter.manga).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -72,6 +72,11 @@ class SettingsLibraryController : SettingsController() {
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.jumpToChapters
|
||||
titleRes = R.string.pref_jump_to_chapters
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<EhSmartS
|
||||
launch(Dispatchers.Default) {
|
||||
for (event in presenter.smartSearchChannel) {
|
||||
if (event is SmartSearchPresenter.SearchResults.Found) {
|
||||
val transaction = if (Injekt.get<PreferencesHelper>().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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user