From 51c8430e9c17f0958f8e33f38ff01d75ce04bdc1 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Thu, 21 May 2020 00:29:59 -0400 Subject: [PATCH] Totally rewrote the all in one manga page, now is a recycler header It works perfect, there is no lag it all --- .../ui/manga/MangaAllInOneAdapter.kt | 81 +++ .../ui/manga/MangaAllInOneController.kt | 559 ++++-------------- .../ui/manga/MangaAllInOneHeaderItem.kt | 48 ++ .../tachiyomi/ui/manga/MangaAllInOneHolder.kt | 440 ++++++++++++++ .../ui/manga/MangaAllInOnePresenter.kt | 26 +- .../ui/manga/chapter/ChapterHolder.kt | 63 ++ .../tachiyomi/ui/manga/chapter/ChapterItem.kt | 49 ++ .../manga_all_in_one_controller.xml | 428 +------------- .../layout-land/manga_all_in_one_header.xml | 381 ++++++++++++ .../layout/manga_all_in_one_controller.xml | 428 +------------- .../res/layout/manga_all_in_one_header.xml | 408 +++++++++++++ 11 files changed, 1633 insertions(+), 1278 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt create mode 100644 app/src/main/res/layout-land/manga_all_in_one_header.xml create mode 100644 app/src/main/res/layout/manga_all_in_one_header.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt new file mode 100644 index 000000000..e0ae1c502 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneAdapter.kt @@ -0,0 +1,81 @@ +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 kotlinx.coroutines.CoroutineScope +import uy.kohesive.injekt.injectLazy + +class MangaAllInOneAdapter( + controller: MangaAllInOneController, + context: Context +) : FlexibleAdapter>(null, controller, true) { + + val delegate: MangaAllInOneInterface = controller + + val preferences: PreferencesHelper by injectLazy() + + var items: List = emptyList() + + val readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f) + val unreadColor = context.getResourceColor(R.attr.colorOnSurface) + + val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) + + val decimalFormat = DecimalFormat( + "#.###", + DecimalFormatSymbols() + .apply { decimalSeparator = '.' } + ) + + val dateFormat: DateFormat = preferences.dateFormat() + + override fun updateDataSet(items: List>?) { + this.items = items as List? ?: emptyList() + super.updateDataSet(items) + } + + fun indexOf(item: MangaAllInOneChapterItem): Int { + return items.indexOf(item) + } + + interface MangaAllInOneInterface : MangaHeaderInterface + + interface MangaHeaderInterface { + fun openSmartSearch() + fun mangaPresenter(): MangaAllInOnePresenter + fun openRecommends() + fun onNextManga(manga: Manga, source: Source, chapters: List) + fun setMangaInfo(manga: Manga, source: Source?, chapters: List) + fun openInWebView() + fun shareManga() + fun fetchMangaFromSource(manualFetch: Boolean = false, fetchManga: Boolean = true, fetchChapters: Boolean = true) + fun onFetchMangaDone() + fun onFetchMangaError(error: Throwable) + fun setRefreshing(value: Boolean) + fun onFavoriteClick() + fun onCategoriesClick() + fun updateCategoriesForMangas(mangas: List, categories: List) + fun performGlobalSearch(query: String) + fun wrapTag(namespace: String, tag: String): String + fun isEHentaiBasedSource(): Boolean + fun performSearch(query: String) + fun openTracking() + suspend fun mergeWithAnother() + fun copyToClipboard(label: String, text: String) + fun migrateManga() + fun isInitialLoadAndFromSource(): Boolean + fun removeInitialLoad() + val controllerScope: CoroutineScope + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt index 4f094261c..9eeb2b8b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneController.kt @@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.ui.manga import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.Activity -import android.content.Context import android.content.Intent import android.os.Bundle -import android.text.TextUtils import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -15,11 +13,9 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode -import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import com.bumptech.glide.load.engine.DiskCacheStrategy import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson import eu.davidea.flexibleadapter.FlexibleAdapter @@ -30,16 +26,11 @@ import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.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.MangaAllInOneControllerBinding 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.source.SourceController @@ -49,12 +40,11 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight -import eu.kanade.tachiyomi.ui.manga.chapter.ChapterHolder -import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter import eu.kanade.tachiyomi.ui.manga.chapter.DeleteChaptersDialog import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog +import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterHolder +import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterItem import eu.kanade.tachiyomi.ui.manga.track.TrackController import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController import eu.kanade.tachiyomi.ui.reader.ReaderActivity @@ -69,13 +59,9 @@ 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 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 @@ -86,11 +72,8 @@ 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 timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -110,7 +93,8 @@ class MangaAllInOneController : FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, DownloadCustomChaptersDialog.Listener, - DeleteChaptersDialog.Listener { + DeleteChaptersDialog.Listener, + MangaAllInOneAdapter.MangaAllInOneInterface { constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: Boolean = false) : super( Bundle().apply { @@ -158,7 +142,7 @@ class MangaAllInOneController : /** * Adapter containing a list of chapters. */ - private var adapter: ChaptersAdapter? = null + private var adapter: MangaAllInOneAdapter? = null /** * Action mode for multiple selection. @@ -168,7 +152,7 @@ class MangaAllInOneController : /** * Selected items. Used to restore selections after a rotation. */ - private val selectedItems = mutableSetOf() + private val selectedItems = mutableSetOf() private var lastClickPosition = -1 @@ -192,6 +176,8 @@ class MangaAllInOneController : var update = args.getBoolean(UPDATE_EXTRA, false) + override val controllerScope = scope + init { setHasOptionsMenu(true) } @@ -214,178 +200,10 @@ class MangaAllInOneController : override fun onViewCreated(view: View) { super.onViewCreated(view) - // Setting this via XML doesn't work - binding.mangaCover.clipToOutline = true - - binding.btnFavorite.clicks() - .onEach { onFavoriteClick() } - .launchIn(scope) - - if ((Injekt.get().hasLoggedServices()) && presenter.manga.favorite) { - binding.btnTracking.visible() - } - - scope.launch(Dispatchers.IO) { - if (Injekt.get().getTracks(presenter.manga).executeAsBlocking().any { - val status = Injekt.get().getService(it.sync_id)?.getStatus(it.status) - status != null - } - ) { - withContext(Dispatchers.Main) { - binding.btnTracking.icon = resources!!.getDrawable(R.drawable.ic_cloud_white_24dp, null) - } - } - } - - binding.btnTracking.clicks() - .onEach { openTracking() } - .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.mangaArtist.longClicks() - .onEach { - activity?.copyToClipboard(binding.mangaArtistLabel.text.toString(), binding.mangaArtist.text.toString()) - } - .launchIn(scope) - - binding.mangaArtist.clicks() - .onEach { - var text = binding.mangaArtist.text.toString() - if (isEHentaiBasedSource()) { - text = wrapTag("artist", text) - } - performGlobalSearch(text) - } - .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!!) - } - - router?.pushController( - MangaAllInOneController( - mergedManga, - true, - update = true - ).withFadeTransaction() - ) - applicationContext?.toast("Manga merged!") - } catch (e: Exception) { - if (e is CancellationException) throw e - else { - applicationContext?.toast("Failed to merge manga: ${e.message}") - } - } - } - } - .launchIn(scope) - } - // EXH <-- - if (manga == null || source == null) return // Init RecyclerView and adapter - adapter = ChaptersAdapter(this, view.context) + adapter = MangaAllInOneAdapter(this, view.context) binding.recycler.adapter = adapter binding.recycler.layoutManager = LinearLayoutManager(view.context) @@ -421,8 +239,28 @@ class MangaAllInOneController : binding.fab.offsetAppbarHeight(activity!!) } + private fun getHeader(): MangaAllInOneHolder? { + return binding.recycler.findViewHolderForAdapterPosition(0) as? MangaAllInOneHolder + } + + private fun addMangaHeader() { + if (adapter?.scrollableHeaders?.isEmpty() == true) { + adapter?.removeAllScrollableHeaders() + adapter?.addScrollableHeader(presenter.headerItem) + } + } + + fun updateHeader() { + // binding.swipeRefresh?.isRefreshing = presenter.isLoading + adapter?.updateDataSet(presenter.chapters) + addMangaHeader() + activity?.invalidateOptionsMenu() + } + + fun refreshAdapter() = adapter?.notifyDataSetChanged() + // EXH --> - private fun openSmartSearch() { + override fun openSmartSearch() { val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.title, presenter.manga.id!!) router?.pushController( @@ -433,10 +271,48 @@ class MangaAllInOneController : ).withFadeTransaction() ) } + + override suspend fun mergeWithAnother() { + try { + val mergedManga = withContext(Dispatchers.IO + NonCancellable) { + presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!) + } + + router?.pushController( + MangaAllInOneController( + mergedManga, + true, + update = true + ).withFadeTransaction() + ) + applicationContext?.toast("Manga merged!") + } catch (e: Exception) { + if (e is CancellationException) throw e + else { + applicationContext?.toast("Failed to merge manga: ${e.message}") + } + } + } + + override fun copyToClipboard(label: String, text: String) { + activity!!.copyToClipboard(label, text) + } + + override fun migrateManga() { + PreMigrationController.navigateToMigration( + preferences.skipPreMigration().get(), + router, + listOf(presenter.manga.id!!) + ) + } + + override fun mangaPresenter(): MangaAllInOnePresenter { + return presenter + } // EXH <-- // AZ --> - private fun openRecommends() { + override fun openRecommends() { val recommendsConfig = BrowseSourceController.RecommendsConfig(presenter.manga) router?.pushController( @@ -449,7 +325,7 @@ class MangaAllInOneController : } // AZ <-- - private fun openTracking() { + override fun openTracking() { router?.pushController( TrackController(fromAllInOne = true, manga = manga).withFadeTransaction() ) @@ -463,7 +339,7 @@ class MangaAllInOneController : * @param manga manga object containing information about manga. * @param source the source of the manga. */ - fun onNextManga(manga: Manga, source: Source, chapters: List) { + override fun onNextManga(manga: Manga, source: Source, chapters: List) { if (manga.initialized) { // Update view. setMangaInfo(manga, source, chapters) @@ -482,121 +358,9 @@ class MangaAllInOneController : * @param manga manga object containing information about manga. * @param source the source of the manga. */ - private fun setMangaInfo(manga: Manga, source: Source?, chapters: List) { + override fun setMangaInfo(manga: Manga, source: Source?, chapters: List) { 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 artist TextView. - binding.mangaArtist.text = if (manga.artist.isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.artist - } - - // Update author TextView. - binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.author - } - - // If manga source is known update source TextView. - val mangaSource = source?.toString() - with(binding.mangaSource) { - // EXH --> - if (mangaSource == null) { - text = view.context.getString(R.string.unknown) - } else if (source.id == MERGED_SOURCE_ID) { - text = MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { - sourceManager.getOrStub(it.source).toString() - }.distinct().joinToString() - } else { - text = mangaSource - setOnClickListener { - val sourceManager = Injekt.get() - performSearch(sourceManager.getOrStub(source.id).name) - } - } - // EXH <-- - } - - // EXH --> - if (source?.id == MERGED_SOURCE_ID) { - binding.mangaSourceLabel.text = "Sources" - } else { - binding.mangaSourceLabel.setText(R.string.manga_info_source_label) - } - // EXH <-- - - // Update status TextView. - binding.mangaStatus.setText( - when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - } - ) - - // Set the favorite drawable to the correct one. - setFavoriteButtonState(manga.favorite) - - // Set cover if it wasn't already. - val mangaThumbnail = manga.toMangaThumbnail() - - GlideApp.with(view.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(binding.mangaCover) - - binding.backdrop?.let { - GlideApp.with(view.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(it) - } - - // Manga info section - if (manga.description.isNullOrBlank() && manga.genre.isNullOrBlank()) { - hideMangaInfo() - } else { - // Update description TextView. - binding.mangaSummary.text = if (manga.description.isNullOrBlank()) { - view.context.getString(R.string.unknown) - } else { - manga.description - } - - // Update genres list - if (!manga.genre.isNullOrBlank()) { - binding.mangaGenresTagsCompactChips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) - binding.mangaGenresTagsFullChips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) - } else { - binding.mangaGenresTagsWrapper.gone() - } - - // Handle showing more or less info - binding.mangaSummary.clicks() - .onEach { toggleMangaInfo(view.context) } - .launchIn(scope) - binding.mangaInfoToggle.clicks() - .onEach { toggleMangaInfo(view.context) } - .launchIn(scope) - - // Expand manga info if navigated from source listing - if (initialLoad && fromSource) { - toggleMangaInfo(view.context) - initialLoad = false - } - } if (update || // Auto-update old format galleries ( @@ -610,6 +374,7 @@ class MangaAllInOneController : val adapter = adapter ?: return adapter.updateDataSet(chapters) + addMangaHeader() if (selectedItems.isNotEmpty()) { adapter.clearSelection() // we need to start from a clean state, index may have changed @@ -624,90 +389,7 @@ class MangaAllInOneController : } } - 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() { + override fun openInWebView() { val source = presenter.source as? HttpSource ?: return val url = try { @@ -724,7 +406,7 @@ class MangaAllInOneController : /** * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. */ - private fun shareManga() { + override fun shareManga() { val context = view?.context ?: return val source = presenter.source as? HttpSource ?: return @@ -740,38 +422,23 @@ class MangaAllInOneController : } } - /** - * 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 - } - } - /** * Start fetching manga information from source. */ - private fun fetchMangaFromSource(manualFetch: Boolean = false, fetchManga: Boolean = true, fetchChapters: Boolean = true) { + override fun fetchMangaFromSource(manualFetch: Boolean, fetchManga: Boolean, fetchChapters: Boolean) { setRefreshing(true) // Call presenter and start fetching manga information presenter.fetchMangaFromSource(manualFetch, fetchManga, fetchChapters) } - fun onFetchMangaDone() { + override fun onFetchMangaDone() { setRefreshing(false) } /** * Update swipe refresh to start showing refresh in progress spinner. */ - fun onFetchMangaError(error: Throwable) { + override fun onFetchMangaError(error: Throwable) { setRefreshing(false) activity?.toast(error.message) } @@ -781,15 +448,15 @@ class MangaAllInOneController : * * @param value whether it should be refreshing or not. */ - fun setRefreshing(value: Boolean) { + override fun setRefreshing(value: Boolean) { binding.swipeRefresh.isRefreshing = value } - private fun onFavoriteClick() { + override fun onFavoriteClick() { val manga = presenter.manga if (manga.favorite) { - toggleFavorite() + getHeader()?.toggleFavorite() activity?.toast(activity?.getString(R.string.manga_removed_library)) } else { val categories = presenter.getCategories() @@ -799,14 +466,14 @@ class MangaAllInOneController : when { // Default category set defaultCategory != null -> { - toggleFavorite() + getHeader()?.toggleFavorite() presenter.moveMangaToCategory(manga, defaultCategory) activity?.toast(activity?.getString(R.string.manga_added_library)) } // Automatic 'Default' or no categories defaultCategoryId == 0 || categories.isEmpty() -> { - toggleFavorite() + getHeader()?.toggleFavorite() presenter.moveMangaToCategory(manga, null) activity?.toast(activity?.getString(R.string.manga_added_library)) } @@ -825,7 +492,7 @@ class MangaAllInOneController : } } - private fun onCategoriesClick() { + override fun onCategoriesClick() { val manga = presenter.manga val categories = presenter.getCategories() @@ -842,7 +509,7 @@ class MangaAllInOneController : val manga = mangas.firstOrNull() ?: return if (!manga.favorite) { - toggleFavorite() + getHeader()?.toggleFavorite() activity?.toast(activity?.getString(R.string.manga_added_library)) } @@ -854,13 +521,31 @@ class MangaAllInOneController : * * @param query the search query to pass to the search controller */ - private fun performGlobalSearch(query: String) { + override fun performGlobalSearch(query: String) { val router = router ?: return router.pushController(GlobalSearchController(query).withFadeTransaction()) } + fun setChapterCount(count: Float) { + getHeader()?.setChapterCount(count) + } + + fun setLastUpdateDate(date: Date) { + getHeader()?.setLastUpdateDate(date) + } + + fun setFavoriteButtonState(isFavorite: Boolean) { + getHeader()?.setFavoriteButtonState(isFavorite) + } + + override fun isInitialLoadAndFromSource() = fromSource && initialLoad + + override fun removeInitialLoad() { + initialLoad = false + } + // --> EH - private fun wrapTag(namespace: String, tag: String) = + override fun wrapTag(namespace: String, tag: String) = if (tag.contains(' ')) { "$namespace:\"$tag$\"" } else { @@ -869,7 +554,7 @@ class MangaAllInOneController : private fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim() - private fun isEHentaiBasedSource(): Boolean { + override fun isEHentaiBasedSource(): Boolean { val sourceId = presenter.source.id return sourceId == EH_SOURCE_ID || sourceId == EXH_SOURCE_ID @@ -881,7 +566,7 @@ class MangaAllInOneController : * * @param query the search query to the previous controller */ - private fun performSearch(query: String) { + override fun performSearch(query: String) { val router = router ?: return if (router.backstackSize < 2) { @@ -1039,8 +724,8 @@ class MangaAllInOneController : getHolder(download.chapter)?.notifyStatus(download.status) } - private fun getHolder(chapter: Chapter): ChapterHolder? { - return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + private fun getHolder(chapter: Chapter): MangaAllInOneChapterHolder? { + return binding.recycler.findViewHolderForItemId(chapter.id!!) as? MangaAllInOneChapterHolder } fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { @@ -1054,7 +739,7 @@ class MangaAllInOneController : override fun onItemClick(view: View?, position: Int): Boolean { val adapter = adapter ?: return false - val item = adapter.getItem(position) ?: return false + val item = adapter.getItem(position) as MangaAllInOneChapterItem? ?: return false return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { lastClickPosition = position toggleSelection(position) @@ -1089,7 +774,7 @@ class MangaAllInOneController : adapter.toggleSelection(position) adapter.notifyDataSetChanged() if (adapter.isSelected(position)) { - selectedItems.add(item) + selectedItems.add(item as MangaAllInOneChapterItem) } else { selectedItems.remove(item) } @@ -1101,14 +786,14 @@ class MangaAllInOneController : val item = adapter.getItem(position) ?: return if (!adapter.isSelected(position)) { adapter.toggleSelection(position) - selectedItems.add(item) + selectedItems.add(item as MangaAllInOneChapterItem) actionMode?.invalidate() } } - private fun getSelectedChapters(): List { + private fun getSelectedChapters(): List { val adapter = adapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as MangaAllInOneChapterItem } } private fun createActionModeIfNeeded() { @@ -1209,24 +894,24 @@ class MangaAllInOneController : for (i in 0..adapter.itemCount) { adapter.toggleSelection(i) } - selectedItems.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) + selectedItems.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) as MangaAllInOneChapterItem }) actionMode?.invalidate() adapter.notifyDataSetChanged() } - private fun markAsRead(chapters: List) { + private fun markAsRead(chapters: List) { presenter.markChaptersRead(chapters, true) if (presenter.preferences.removeAfterMarkedAsRead()) { deleteChapters(chapters) } } - private fun markAsUnread(chapters: List) { + private fun markAsUnread(chapters: List) { presenter.markChaptersRead(chapters, false) } - private fun downloadChapters(chapters: List) { + private fun downloadChapters(chapters: List) { val view = view presenter.downloadChapters(chapters) if (view != null && !presenter.manga.favorite) { @@ -1246,7 +931,7 @@ class MangaAllInOneController : deleteChapters(getSelectedChapters()) } - private fun markPreviousAsRead(chapters: List) { + private fun markPreviousAsRead(chapters: List) { val adapter = adapter ?: return val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items val chapterPos = prevChapters.indexOf(chapters.last()) @@ -1255,17 +940,17 @@ class MangaAllInOneController : } } - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { presenter.bookmarkChapters(chapters, bookmarked) } - fun deleteChapters(chapters: List) { + fun deleteChapters(chapters: List) { if (chapters.isEmpty()) return presenter.deleteChapters(chapters) } - fun onChaptersDeleted(chapters: List) { + fun onChaptersDeleted(chapters: List) { // this is needed so the downloaded text gets removed from the item chapters.forEach { adapter?.updateItem(it) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt new file mode 100644 index 000000000..f8d48e9e7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHeaderItem.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.browse.source.SourceController + +class MangaAllInOneHeaderItem(val manga: Manga, val source: Source, var smartSearchConfig: SourceController.SmartSearchConfig? = null) : + AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.manga_all_in_one_header + } + + override fun isSelectable(): Boolean { + return false + } + + override fun isSwipeable(): Boolean { + return false + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MangaAllInOneHolder { + return MangaAllInOneHolder(view, adapter as MangaAllInOneAdapter, smartSearchConfig) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: MangaAllInOneHolder, + position: Int, + payloads: MutableList? + ) { + holder.bind(this, manga, source) + } + + override fun equals(other: Any?): Boolean { + return (this === other) + } + + override fun hashCode(): Int { + return -(manga.id).hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt new file mode 100644 index 000000000..53f969923 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOneHolder.kt @@ -0,0 +1,440 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.content.Context +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.view.accessibility.AccessibilityEventCompat.setAction +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.PreferenceKeys.dateFormat +import eu.kanade.tachiyomi.data.track.TrackManager +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.debug.DebugFunctions.sourceManager +import exh.util.setChipsExtended +import java.text.DecimalFormat +import java.util.Date +import kotlinx.android.synthetic.main.manga_all_in_one_header.backdrop +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_categories +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_favorite +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_migrate +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_share +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_smart_search +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_tracking +import kotlinx.android.synthetic.main.manga_all_in_one_header.btn_webview +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_artist +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_artist_label +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_author +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_author_label +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_chapters +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_cover +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_full_title +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_genres_tags_compact +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_genres_tags_compact_chips +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_genres_tags_full_chips +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_genres_tags_wrapper +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_info_toggle +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_last_update +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_source +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_source_label +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_status +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_summary +import kotlinx.android.synthetic.main.manga_all_in_one_header.manga_summary_label +import kotlinx.android.synthetic.main.manga_all_in_one_header.merge_btn +import kotlinx.android.synthetic.main.manga_all_in_one_header.recommend_btn +import kotlinx.coroutines.Dispatchers +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 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 gson: Gson by injectLazy() + + init { + val presenter = adapter.delegate.mangaPresenter() + + // Setting this via XML doesn't work + manga_cover.clipToOutline = true + + btn_favorite.clicks() + .onEach { adapter.delegate.onFavoriteClick() } + .launchIn(adapter.delegate.controllerScope) + + if ((Injekt.get().hasLoggedServices()) && presenter.manga.favorite) { + btn_tracking.visible() + } + + adapter.delegate.controllerScope.launch(Dispatchers.IO) { + if (Injekt.get().getTracks(presenter.manga).executeAsBlocking().any { + val status = Injekt.get().getService(it.sync_id)?.getStatus(it.status) + status != null + } + ) { + withContext(Dispatchers.Main) { + btn_tracking.icon = itemView.context.getDrawable(R.drawable.ic_cloud_white_24dp) + } + } + } + + btn_tracking.clicks() + .onEach { adapter.delegate.openTracking() } + .launchIn(adapter.delegate.controllerScope) + + if (presenter.manga.favorite && presenter.getCategories().isNotEmpty()) { + btn_categories.visible() + } + btn_categories.clicks() + .onEach { adapter.delegate.onCategoriesClick() } + .launchIn(adapter.delegate.controllerScope) + + if (presenter.source is HttpSource) { + btn_webview.visible() + btn_share.visible() + + btn_webview.clicks() + .onEach { adapter.delegate.openInWebView() } + .launchIn(adapter.delegate.controllerScope) + btn_share.clicks() + .onEach { adapter.delegate.shareManga() } + .launchIn(adapter.delegate.controllerScope) + } + + if (presenter.manga.favorite) { + btn_migrate.visible() + btn_smart_search.visible() + } + + btn_migrate.clicks() + .onEach { + adapter.delegate.migrateManga() + } + .launchIn(adapter.delegate.controllerScope) + + btn_smart_search.clicks() + .onEach { adapter.delegate.openSmartSearch() } + .launchIn(adapter.delegate.controllerScope) + + manga_full_title.longClicks() + .onEach { + adapter.delegate.copyToClipboard(view.context.getString(R.string.title), manga_full_title.text.toString()) + } + .launchIn(adapter.delegate.controllerScope) + + manga_full_title.clicks() + .onEach { + adapter.delegate.performGlobalSearch(manga_full_title.text.toString()) + } + .launchIn(adapter.delegate.controllerScope) + + manga_artist.longClicks() + .onEach { + adapter.delegate.copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString()) + } + .launchIn(adapter.delegate.controllerScope) + + manga_artist.clicks() + .onEach { + var text = manga_artist.text.toString() + if (adapter.delegate.isEHentaiBasedSource()) { + text = adapter.delegate.wrapTag("artist", text) + } + adapter.delegate.performGlobalSearch(text) + } + .launchIn(adapter.delegate.controllerScope) + + manga_author.longClicks() + .onEach { + // EXH Special case E-Hentai/ExHentai to ignore author field (unused) + if (!adapter.delegate.isEHentaiBasedSource()) { + adapter.delegate.copyToClipboard(manga_author_label.text.toString(), manga_author.text.toString()) + } + } + .launchIn(adapter.delegate.controllerScope) + + manga_author.clicks() + .onEach { + // EXH Special case E-Hentai/ExHentai to ignore author field (unused) + if (!adapter.delegate.isEHentaiBasedSource()) { + adapter.delegate.performGlobalSearch(manga_author.text.toString()) + } + } + .launchIn(adapter.delegate.controllerScope) + + manga_summary.longClicks() + .onEach { + adapter.delegate.copyToClipboard(view.context.getString(R.string.description), manga_summary.text.toString()) + } + .launchIn(adapter.delegate.controllerScope) + + manga_cover.longClicks() + .onEach { + adapter.delegate.copyToClipboard(view.context.getString(R.string.title), presenter.manga.title) + } + .launchIn(adapter.delegate.controllerScope) + + // EXH --> + if (smartSearchConfig == null) { + recommend_btn.visible() + recommend_btn.clicks() + .onEach { adapter.delegate.openRecommends() } + .launchIn(adapter.delegate.controllerScope) + } + smartSearchConfig?.let { smartSearchConfig -> + if (smartSearchConfig.origMangaId != null) { merge_btn.visible() } + merge_btn.clicks() + .onEach { + adapter.delegate.mergeWithAnother() + } + + .launchIn(adapter.delegate.controllerScope) + } + // EXH <-- + } + + fun bind(item: MangaAllInOneHeaderItem, manga: Manga, source: Source?) { + val presenter = adapter.delegate.mangaPresenter() + + manga_full_title.text = if (manga.title.isBlank()) { + itemView.context.getString(R.string.unknown) + } else { + manga.title + } + + // Update artist TextView. + manga_artist.text = if (manga.artist.isNullOrBlank()) { + itemView.context.getString(R.string.unknown) + } else { + manga.artist + } + + // Update author TextView. + manga_author.text = if (manga.author.isNullOrBlank()) { + itemView.context.getString(R.string.unknown) + } else { + manga.author + } + + // If manga source is known update source TextView. + val mangaSource = source?.toString() + with(manga_source) { + // EXH --> + if (mangaSource == null) { + text = itemView.context.getString(R.string.unknown) + } else if (source.id == MERGED_SOURCE_ID) { + text = eu.kanade.tachiyomi.source.online.all.MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { + sourceManager.getOrStub(it.source).toString() + }.distinct().joinToString() + } else { + text = mangaSource + setOnClickListener { + val sourceManager = Injekt.get() + adapter.delegate.performSearch(sourceManager.getOrStub(source.id).name) + } + } + // EXH <-- + } + + // EXH --> + if (source?.id == MERGED_SOURCE_ID) { + manga_source_label.text = "Sources" + } else { + manga_source_label.setText(R.string.manga_info_source_label) + } + // EXH <-- + + // Update status TextView. + manga_status.setText( + when (manga.status) { + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed + else -> R.string.unknown + } + ) + + // Set the favorite drawable to the correct one. + 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(manga_cover) + + backdrop?.let { + GlideApp.with(itemView.context) + .load(mangaThumbnail) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(it) + } + + // Manga info section + if (manga.description.isNullOrBlank() && manga.genre.isNullOrBlank()) { + hideMangaInfo() + } else { + // Update description TextView. + manga_summary.text = if (manga.description.isNullOrBlank()) { + itemView.context.getString(R.string.unknown) + } else { + manga.description + } + + // Update genres list + if (!manga.genre.isNullOrBlank()) { + manga_genres_tags_compact_chips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) + manga_genres_tags_full_chips.setChipsExtended(manga.getGenres(), this::performSearch, this::performGlobalSearch, manga.source) + } else { + manga_genres_tags_wrapper.gone() + } + + // Handle showing more or less info + manga_summary.clicks() + .onEach { toggleMangaInfo(itemView.context) } + .launchIn(adapter.delegate.controllerScope) + manga_info_toggle.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() { + manga_summary_label.gone() + manga_summary.gone() + manga_genres_tags_wrapper.gone() + manga_info_toggle.gone() + } + + fun toggleMangaInfo(context: Context) { + val isExpanded = manga_info_toggle.text == context.getString(R.string.manga_info_collapse) + + manga_info_toggle.text = + if (isExpanded) { + context.getString(R.string.manga_info_expand) + } else { + context.getString(R.string.manga_info_collapse) + } + + with(manga_summary) { + maxLines = + if (isExpanded) { + 3 + } else { + Int.MAX_VALUE + } + + ellipsize = + if (isExpanded) { + android.text.TextUtils.TruncateAt.END + } else { + null + } + } + + manga_genres_tags_compact.visibleIf { isExpanded } + manga_genres_tags_full_chips.visibleIf { !isExpanded } + } + + /** + * Update chapter count TextView. + * + * @param count number of chapters. + */ + fun setChapterCount(count: Float) { + if (count > 0f) { + manga_chapters.text = DecimalFormat("#.#").format(count) + } else { + manga_chapters.text = itemView.context.getString(R.string.unknown) + } + } + + fun setLastUpdateDate(date: Date) { + if (date.time != 0L) { + manga_last_update.text = dateFormat.format(date) + } else { + manga_last_update.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 (itemView != null && !isNowFavorite && presenter.hasDownloads()) { + itemView.snack(itemView.context.getString(R.string.delete_downloads_for_manga)) { + setAction(R.string.action_delete) { + presenter.deleteDownloads() + } + } + } + + btn_categories.visibleIf { isNowFavorite && presenter.getCategories().isNotEmpty() } + if (isNowFavorite) { + btn_smart_search.visible() + btn_migrate.visible() + } else { + btn_smart_search.gone() + btn_migrate.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. + btn_favorite.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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt index 6090a4f93..581d6be90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaAllInOnePresenter.kt @@ -18,8 +18,8 @@ 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.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter +import eu.kanade.tachiyomi.ui.manga.chapter.MangaAllInOneChapterItem import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.removeCovers @@ -64,7 +64,7 @@ class MangaAllInOnePresenter( /** * List of chapters of the manga. It's always unfiltered and unsorted. */ - var chapters: List = emptyList() + var chapters: List = emptyList() private set private val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -86,6 +86,8 @@ class MangaAllInOnePresenter( private val redirectUserRelay = BehaviorRelay.create() // EXH <-- + var headerItem = MangaAllInOneHeaderItem(manga, source, smartSearchConfig) + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -385,9 +387,9 @@ class MangaAllInOnePresenter( /** * Converts a chapter from the database to an extended model, allowing to store new fields. */ - private fun Chapter.toModel(): ChapterItem { + private fun Chapter.toModel(): MangaAllInOneChapterItem { // Create the model object. - val model = ChapterItem(this, manga) + val model = MangaAllInOneChapterItem(this, manga) // Find an active download for this chapter. val download = downloadManager.queue.find { it.chapter.id == id } @@ -404,7 +406,7 @@ class MangaAllInOnePresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(chapters: List) { + private fun setDownloadedChapters(chapters: List) { for (chapter in chapters) { if (downloadManager.isChapterDownloaded(chapter, manga)) { chapter.status = Download.DOWNLOADED @@ -417,7 +419,7 @@ class MangaAllInOnePresenter( * @param chapters the list of chapters from the database * @return an observable of the list of chapters filtered and sorted. */ - private fun applyChapterFilters(chapterList: List): List { + private fun applyChapterFilters(chapterList: List): List { var chapters = chapterList if (onlyUnread()) { chapters = chapters.filter { !it.read } @@ -468,7 +470,7 @@ class MangaAllInOnePresenter( /** * Returns the next unread chapter or null if everything is read. */ - fun getNextUnreadChapter(): ChapterItem? { + fun getNextUnreadChapter(): MangaAllInOneChapterItem? { return chapters.sortedByDescending { it.source_order }.find { !it.read } } @@ -477,7 +479,7 @@ class MangaAllInOnePresenter( * @param selectedChapters the list of selected chapters. * @param read whether to mark chapters as read or unread. */ - fun markChaptersRead(selectedChapters: List, read: Boolean) { + fun markChaptersRead(selectedChapters: List, read: Boolean) { Observable.from(selectedChapters) .doOnNext { chapter -> chapter.read = read @@ -498,7 +500,7 @@ class MangaAllInOnePresenter( * Downloads the given list of chapters with the manager. * @param chapters the list of chapters to download. */ - fun downloadChapters(chapters: List) { + fun downloadChapters(chapters: List) { downloadManager.downloadChapters(manga, chapters) } @@ -506,7 +508,7 @@ class MangaAllInOnePresenter( * Bookmarks the given list of chapters. * @param selectedChapters the list of chapters to bookmark. */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { Observable.from(selectedChapters) .doOnNext { chapter -> chapter.bookmark = bookmarked @@ -521,7 +523,7 @@ class MangaAllInOnePresenter( * Deletes the given list of chapter. * @param chapters the list of chapters to delete. */ - fun deleteChapters(chapters: List) { + fun deleteChapters(chapters: List) { Observable.just(chapters) .doOnNext { deleteChaptersInternal(chapters) } .doOnNext { if (onlyDownloaded()) updateChaptersView() } @@ -539,7 +541,7 @@ class MangaAllInOnePresenter( * Deletes a list of chapters from disk. This method is called in a background thread. * @param chapters the chapters to delete. */ - private fun deleteChaptersInternal(chapters: List) { + private fun deleteChaptersInternal(chapters: List) { downloadManager.deleteChapters(chapters, manga, source) chapters.forEach { it.status = Download.NOT_DOWNLOADED diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt index 4f4703e0f..1f2491c56 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt @@ -8,6 +8,7 @@ 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 @@ -76,3 +77,65 @@ class ChapterHolder( } } } + +class MangaAllInOneChapterHolder( + view: View, + private val adapter: MangaAllInOneAdapter +) : BaseFlexibleViewHolder(view, adapter) { + + fun bind(item: MangaAllInOneChapterItem, manga: Manga) { + val chapter = item.chapter + + chapter_title.text = when (manga.displayMode) { + Manga.DISPLAY_NUMBER -> { + val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) + itemView.context.getString(R.string.display_mode_chapter, number) + } + else -> chapter.name + } + + // Set correct text color + val chapterColor = when { + chapter.read -> adapter.readColor + chapter.bookmark -> adapter.bookmarkedColor + else -> adapter.unreadColor + } + chapter_title.setTextColor(chapterColor) + chapter_description.setTextColor(chapterColor) + + bookmark_icon.visibleIf { chapter.bookmark } + + val descriptions = mutableListOf() + + if (chapter.date_upload > 0) { + descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload))) + } + if (!chapter.read && chapter.last_page_read > 0) { + val lastPageRead = SpannableString(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)).apply { + setSpan(ForegroundColorSpan(adapter.readColor), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) + } + descriptions.add(lastPageRead) + } + if (!chapter.scanlator.isNullOrBlank()) { + descriptions.add(chapter.scanlator!!) + } + + if (descriptions.isNotEmpty()) { + chapter_description.text = descriptions.joinTo(SpannableStringBuilder(), " • ") + } else { + chapter_description.text = "" + } + + notifyStatus(item.status) + } + + fun notifyStatus(status: Int) = with(download_text) { + when (status) { + Download.QUEUE -> setText(R.string.chapter_queued) + Download.DOWNLOADING -> setText(R.string.chapter_downloading) + Download.DOWNLOADED -> setText(R.string.chapter_downloaded) + Download.ERROR -> setText(R.string.chapter_error) + else -> text = "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt index 649cab753..53acd7f5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt @@ -9,6 +9,7 @@ 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(), @@ -57,3 +58,51 @@ class ChapterItem(val chapter: Chapter, val manga: Manga) : return chapter.id!!.hashCode() } } + +class MangaAllInOneChapterItem(val chapter: Chapter, val manga: Manga) : + AbstractFlexibleItem(), + Chapter by chapter { + + private var _status: Int = 0 + + var status: Int + get() = download?.status ?: _status + set(value) { + _status = value + } + + @Transient + var download: Download? = null + + val isDownloaded: Boolean + get() = status == Download.DOWNLOADED + + override fun getLayoutRes(): Int { + return R.layout.chapters_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MangaAllInOneChapterHolder { + return MangaAllInOneChapterHolder(view, adapter as MangaAllInOneAdapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: MangaAllInOneChapterHolder, + position: Int, + payloads: List? + ) { + holder.bind(this, manga) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is MangaAllInOneChapterItem) { + return chapter.id!! == other.chapter.id!! + } + return false + } + + override fun hashCode(): Int { + return chapter.id!!.hashCode() + } +} diff --git a/app/src/main/res/layout-land/manga_all_in_one_controller.xml b/app/src/main/res/layout-land/manga_all_in_one_controller.xml index 64960d5dd..f2176199b 100644 --- a/app/src/main/res/layout-land/manga_all_in_one_controller.xml +++ b/app/src/main/res/layout-land/manga_all_in_one_controller.xml @@ -14,425 +14,29 @@ android:elevation="5dp" android:visibility="invisible" /> - - - + android:layout_height="match_parent" + tools:context=".ui.browse.source.browse.BrowseSourceController"> - + - - - + android:clipToPadding="false" + android:descendantFocusability="blocksDescendants" + android:paddingBottom="@dimen/fab_list_padding" + tools:listitem="@layout/chapters_item" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -