From 23fe848a35113252247e7538feba86509f060a60 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 11 Jul 2020 12:13:05 -0400 Subject: [PATCH] Move tracking to manga info actions Currently just opens a separate view. To be iterated upon later. --- .../globalsearch/GlobalSearchController.kt | 1 - .../tachiyomi/ui/manga/MangaController.kt | 897 ++++++++++++++++-- ...ChaptersPresenter.kt => MangaPresenter.kt} | 17 +- .../ui/manga/chapter/ChaptersAdapter.kt | 3 +- .../chapter/MangaInfoChaptersController.kt | 859 ----------------- .../{chapter => info}/MangaCoverImageView.kt | 2 +- .../MangaInfoHeaderAdapter.kt | 25 +- .../ui/manga/track/TrackController.kt | 36 +- app/src/main/res/layout/manga_info_header.xml | 17 +- 9 files changed, 902 insertions(+), 955 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/ui/manga/{chapter/MangaInfoChaptersPresenter.kt => MangaPresenter.kt} (97%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersController.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/manga/{chapter => info}/MangaCoverImageView.kt (94%) rename app/src/main/java/eu/kanade/tachiyomi/ui/manga/{chapter => info}/MangaInfoHeaderAdapter.kt (93%) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt index ba8ab206c1..021427ea14 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt @@ -75,7 +75,6 @@ open class GlobalSearchController( * @param manga clicked item containing manga information. */ override fun onMangaClick(manga: Manga) { - // Open MangaController. router.pushController(MangaController(manga, true).withFadeTransaction()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 9762578538..e9871693b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -1,37 +1,84 @@ package eu.kanade.tachiyomi.ui.manga -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.core.graphics.drawable.DrawableCompat +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import com.bluelinelabs.conductor.Router -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.support.RouterPagerAdapter -import com.google.android.material.tabs.TabLayout -import com.jakewharton.rxrelay.BehaviorRelay +import com.google.android.material.snackbar.Snackbar +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.databinding.PagerControllerBinding +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.databinding.ChaptersControllerBinding +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.RxController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController -import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe -import eu.kanade.tachiyomi.ui.manga.chapter.MangaInfoChaptersController +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterHolder +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter +import eu.kanade.tachiyomi.ui.manga.chapter.DeleteChaptersDialog +import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog +import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter +import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter import eu.kanade.tachiyomi.ui.manga.track.TrackController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.recent.history.HistoryController +import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController +import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.toast -import kotlinx.android.synthetic.main.main_activity.tabs -import rx.Subscription +import eu.kanade.tachiyomi.util.view.getCoordinates +import eu.kanade.tachiyomi.util.view.gone +import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import eu.kanade.tachiyomi.util.view.snack +import eu.kanade.tachiyomi.util.view.visible +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks +import reactivecircus.flowbinding.swiperefreshlayout.refreshes +import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy -class MangaController : RxController, TabbedController { +class MangaController : + NucleusController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ChangeMangaCategoriesDialog.Listener, + DownloadCustomChaptersDialog.Listener, + DeleteChaptersDialog.Listener { constructor(manga: Manga?, fromSource: Boolean = false) : super( Bundle().apply { @@ -58,20 +105,48 @@ class MangaController : RxController, TabbedController { var source: Source? = null private set - private var adapter: MangaDetailAdapter? = null + private val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) - val fromSource = args.getBoolean(FROM_SOURCE_EXTRA, false) + private val preferences: PreferencesHelper by injectLazy() - private val trackingIconRelay: BehaviorRelay = BehaviorRelay.create() + private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null + private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null + private var chaptersAdapter: ChaptersAdapter? = null - private var trackingIconSubscription: Subscription? = null + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Selected items. Used to restore selections after a rotation. + */ + private val selectedChapters = mutableSetOf() + + private val isLocalSource by lazy { presenter.source.id == LocalSource.ID } + + private var lastClickPosition = -1 + + private var isRefreshingInfo = false + private var isRefreshingChapters = false + + init { + setHasOptionsMenu(true) + } override fun getTitle(): String? { return manga?.title } + override fun createPresenter(): MangaPresenter { + return MangaPresenter( + manga!!, + source!! + ) + } + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - binding = PagerControllerBinding.inflate(inflater) + binding = ChaptersControllerBinding.inflate(inflater) return binding.root } @@ -80,23 +155,65 @@ class MangaController : RxController, TabbedController { if (manga == null || source == null) return - requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) + // Init RecyclerView and adapter + mangaInfoAdapter = + MangaInfoHeaderAdapter( + this, + fromSource + ) + chaptersHeaderAdapter = + MangaChaptersHeaderAdapter() + chaptersAdapter = ChaptersAdapter( + this, + view.context + ) - adapter = MangaDetailAdapter() - binding.pager.adapter = adapter - } + binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) + binding.recycler.layoutManager = LinearLayoutManager(view.context) + binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recycler.setHasFixedSize(true) + chaptersAdapter?.fastScroller = binding.fastScroller - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (type.isEnter) { - activity?.tabs?.setupWithViewPager(binding.pager) - trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) } + // Skips directly to chapters list if navigated to from the library + binding.recycler.post { + if (!fromSource && preferences.jumpToChapters()) { + (binding.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0) + } } + + binding.swipeRefresh.refreshes() + .onEach { + fetchMangaInfoFromSource(manualFetch = true) + 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 onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { @@ -107,68 +224,704 @@ class MangaController : RxController, TabbedController { } } - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_FILL - tabMode = TabLayout.MODE_FIXED + override fun onDestroyView(view: View) { + destroyActionModeIfNeeded() + binding.actionToolbar.destroy() + mangaInfoAdapter = null + chaptersHeaderAdapter = null + chaptersAdapter = null + super.onDestroyView(view) + } + + override fun onActivityResumed(activity: Activity) { + if (view == null) return + + // Check if animation view is visible + if (binding.revealView.visibility == View.VISIBLE) { + // Show the unreveal effect + val coordinates = binding.fab.getCoordinates() + binding.revealView.hideRevealEffect(coordinates.x, coordinates.y, 1920) } + + super.onActivityResumed(activity) } - override fun cleanupTabs(tabs: TabLayout) { - trackingIconSubscription?.unsubscribe() - setTrackingIconInternal(false) + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.chapters, menu) } - fun setTrackingIcon(visible: Boolean) { - trackingIconRelay.call(visible) - } + 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) - private fun setTrackingIconInternal(visible: Boolean) { - val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return - val drawable = if (visible) { - VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) + // Set correct checkbox values. + menuFilterRead.isChecked = presenter.onlyRead() + menuFilterUnread.isChecked = presenter.onlyUnread() + menuFilterDownloaded.isChecked = presenter.onlyDownloaded() + menuFilterDownloaded.isEnabled = !presenter.forceDownloaded() + menuFilterBookmarked.isChecked = presenter.onlyBookmarked() + + val filterSet = presenter.onlyRead() || presenter.onlyUnread() || presenter.onlyDownloaded() || presenter.onlyBookmarked() + if (filterSet) { + val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) + DrawableCompat.setTint(menu.findItem(R.id.action_filter).icon, filterColor) + } + + // Only show remove filter option if there's a filter set. + menuFilterEmpty.isVisible = filterSet + + // Display mode submenu + if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { + menu.findItem(R.id.display_title).isChecked = true } else { - null + menu.findItem(R.id.display_chapter_number).isChecked = true } - tab.icon = drawable + // Sorting mode submenu + val sortingItem = when (presenter.manga.sorting) { + Manga.SORTING_SOURCE -> R.id.sort_by_source + Manga.SORTING_NUMBER -> R.id.sort_by_number + Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date + else -> throw NotImplementedError("Unimplemented sorting method") + } + menu.findItem(sortingItem).isChecked = true + menu.findItem(R.id.action_sort_descending).isChecked = presenter.manga.sortDescending() + + // Hide download options for local manga + menu.findItem(R.id.download_group).isVisible = !isLocalSource } - private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { + 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) + } - private val tabTitles = listOf( - R.string.manga_chapters_tab, - R.string.manga_tracking_tab - ) - .map { resources!!.getString(it) } + R.id.sort_by_source -> { + item.isChecked = true + presenter.setSorting(Manga.SORTING_SOURCE) + } + R.id.sort_by_number -> { + item.isChecked = true + presenter.setSorting(Manga.SORTING_NUMBER) + } + R.id.sort_by_upload_date -> { + item.isChecked = true + presenter.setSorting(Manga.SORTING_UPLOAD_DATE) + } + R.id.action_sort_descending -> { + presenter.reverseSortOrder() + activity?.invalidateOptionsMenu() + } - private val tabCount = tabTitles.size - if (Injekt.get().hasLoggedServices()) 0 else 1 + 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) - override fun getCount(): Int { - return tabCount + 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_migrate -> migrateManga() + } + return super.onOptionsItemSelected(item) + } + + private fun updateRefreshing() { + binding.swipeRefresh.isRefreshing = isRefreshingInfo || isRefreshingChapters + } + + // Manga info - start + + /** + * Check if manga is initialized. + * If true update header with manga information, + * if false fetch manga information + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun onNextMangaInfo(manga: Manga, source: Source) { + if (manga.initialized) { + // Update view. + mangaInfoAdapter?.update(manga, source) + } else { + // Initialize manga. + fetchMangaInfoFromSource() + } + } + + /** + * Start fetching manga information from source. + */ + private fun fetchMangaInfoFromSource(manualFetch: Boolean = false) { + isRefreshingInfo = true + updateRefreshing() + + // Call presenter and start fetching manga information + presenter.fetchMangaFromSource(manualFetch) + } + + fun onFetchMangaInfoDone() { + isRefreshingInfo = false + updateRefreshing() + } + + fun onFetchMangaInfoError(error: Throwable) { + isRefreshingInfo = false + updateRefreshing() + activity?.toast(error.message) + } + + fun openMangaInWebView() { + val source = presenter.source as? HttpSource ?: return + + val url = try { + source.mangaDetailsRequest(presenter.manga).url.toString() + } catch (e: Exception) { + return } - override fun configureRouter(router: Router, position: Int) { - if (!router.hasRootController()) { - val controller = when (position) { - INFO_CHAPTERS_CONTROLLER -> MangaInfoChaptersController(fromSource) - TRACK_CONTROLLER -> TrackController() - else -> error("Wrong position $position") + val activity = activity ?: return + val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title) + startActivity(intent) + } + + fun shareManga() { + val context = view?.context ?: return + + val source = presenter.source as? HttpSource ?: return + try { + val url = source.mangaDetailsRequest(presenter.manga).url.toString() + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + fun onFavoriteClick() { + val manga = presenter.manga + + if (manga.favorite) { + toggleFavorite() + activity?.toast(activity?.getString(R.string.manga_removed_library)) + } else { + addToLibrary(manga) + } + } + + fun onTrackingClick() { + router.pushController(TrackController(manga).withFadeTransaction()) + } + + private fun addToLibrary(manga: Manga) { + val categories = presenter.getCategories() + val defaultCategoryId = preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } + + when { + // Default category set + defaultCategory != null -> { + toggleFavorite() + presenter.moveMangaToCategory(manga, defaultCategory) + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + toggleFavorite() + presenter.moveMangaToCategory(manga, null) + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + + // Choose a category + else -> { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + } + + /** + * 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() } - router.setRoot(RouterTransaction.with(controller)) } } - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] + mangaInfoAdapter?.notifyDataSetChanged() + } + + fun onCategoriesClick() { + val manga = presenter.manga + val categories = presenter.getCategories() + + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + + if (!manga.favorite) { + toggleFavorite() + activity?.toast(activity?.getString(R.string.manga_added_library)) + } + + presenter.moveMangaToCategories(manga, categories) + } + + /** + * Perform a global search using the provided query. + * + * @param query the search query to pass to the search controller + */ + fun performGlobalSearch(query: String) { + router.pushController(GlobalSearchController(query).withFadeTransaction()) + } + + /** + * Perform a search using the provided query. + * + * @param query the search query to the parent controller + */ + fun performSearch(query: String) { + if (router.backstackSize < 2) { + return + } + + when (val previousController = router.backstack[router.backstackSize - 2].controller()) { + is LibraryController -> { + router.handleBack() + previousController.search(query) + } + is UpdatesController, + is HistoryController -> { + // Manually navigate to LibraryController + router.handleBack() + (router.activity as MainActivity).setSelectedNavItem(R.id.nav_library) + val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController + controller.search(query) + } + is BrowseSourceController -> { + router.handleBack() + previousController.searchWithQuery(query) + } } } + // Manga info - end + + // Chapters list - start + + /** + * Initiates source migration for the specific manga. + */ + private fun migrateManga() { + val controller = + SearchController( + presenter.manga + ) + controller.targetController = this + router.pushController(controller.withFadeTransaction()) + } + + fun onNextChapters(chapters: List) { + // If the list is empty and it hasn't requested previously, fetch chapters from source + // We use presenter chapters instead because they are always unfiltered + if (!presenter.hasRequested && presenter.chapters.isEmpty()) { + fetchChaptersFromSource() + } + + val chaptersHeader = chaptersHeaderAdapter ?: return + chaptersHeader.setNumChapters(chapters.size) + + val adapter = chaptersAdapter ?: return + adapter.updateDataSet(chapters) + + if (selectedChapters.isNotEmpty()) { + adapter.clearSelection() // we need to start from a clean state, index may have changed + createActionModeIfNeeded() + selectedChapters.forEach { item -> + val position = adapter.indexOf(item) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + } + } + actionMode?.invalidate() + } + + val context = view?.context + if (context != null && chapters.any { it.read }) { + binding.fab.text = context.getString(R.string.action_resume) + } + } + + private fun fetchChaptersFromSource(manualFetch: Boolean = false) { + isRefreshingChapters = true + updateRefreshing() + + presenter.fetchChaptersFromSource(manualFetch) + } + + fun onFetchChaptersDone() { + isRefreshingChapters = false + updateRefreshing() + } + + fun onFetchChaptersError(error: Throwable) { + isRefreshingChapters = false + updateRefreshing() + activity?.toast(error.message) + } + + fun onChapterStatusChange(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status) + } + + private fun getHolder(chapter: Chapter): ChapterHolder? { + return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + } + + fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) + if (hasAnimation) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + } + startActivity(intent) + } + + override fun onItemClick(view: View?, position: Int): Boolean { + val adapter = chaptersAdapter ?: return false + val item = adapter.getItem(position) ?: return false + return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { + lastClickPosition = position + toggleSelection(position) + true + } else { + openChapter(item.chapter) + false + } + } + + override fun onItemLongClick(position: Int) { + createActionModeIfNeeded() + when { + lastClickPosition == -1 -> setSelection(position) + lastClickPosition > position -> + for (i in position until lastClickPosition) + setSelection(i) + lastClickPosition < position -> + for (i in lastClickPosition + 1..position) + setSelection(i) + else -> setSelection(position) + } + lastClickPosition = position + chaptersAdapter?.notifyDataSetChanged() + } + + // SELECTIONS & ACTION MODE + + private fun toggleSelection(position: Int) { + val adapter = chaptersAdapter ?: return + val item = adapter.getItem(position) ?: return + adapter.toggleSelection(position) + adapter.notifyDataSetChanged() + if (adapter.isSelected(position)) { + selectedChapters.add(item) + } else { + selectedChapters.remove(item) + } + actionMode?.invalidate() + } + + private fun setSelection(position: Int) { + val adapter = chaptersAdapter ?: return + val item = adapter.getItem(position) ?: return + if (!adapter.isSelected(position)) { + adapter.toggleSelection(position) + selectedChapters.add(item) + actionMode?.invalidate() + } + } + + private fun getSelectedChapters(): List { + val adapter = chaptersAdapter ?: return emptyList() + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } + } + + private fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) + binding.actionToolbar.show( + actionMode!!, + R.menu.chapter_selection + ) { onActionItemClicked(it!!) } + } + } + + private fun destroyActionModeIfNeeded() { + lastClickPosition = -1 + actionMode?.finish() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.generic_selection, menu) + chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = chaptersAdapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = count.toString() + + val chapters = getSelectedChapters() + binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && chapters.any { !it.isDownloaded } + binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && chapters.any { it.isDownloaded } + binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } + binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } + binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } + binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } + + // Hide FAB to avoid interfering with the bottom action toolbar + // 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() + chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE + chaptersAdapter?.clearSelection() + selectedChapters.clear() + actionMode = null + + // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] + // fails to show up properly + // binding.fab.show() + binding.fab.visible() + } + + override fun onDetach(view: View) { + destroyActionModeIfNeeded() + super.onDetach(view) + } + + // SELECTION MODE ACTIONS + + private fun selectAll() { + val adapter = chaptersAdapter ?: return + adapter.selectAll() + selectedChapters.addAll(adapter.items) + actionMode?.invalidate() + } + + private fun selectInverse() { + val adapter = chaptersAdapter ?: return + + selectedChapters.clear() + for (i in 0..adapter.itemCount) { + adapter.toggleSelection(i) + } + selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) + + actionMode?.invalidate() + adapter.notifyDataSetChanged() + } + + private fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + destroyActionModeIfNeeded() + } + + private fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + destroyActionModeIfNeeded() + } + + private fun downloadChapters(chapters: List) { + val view = view + val manga = presenter.manga + presenter.downloadChapters(chapters) + if (view != null && !manga.favorite) { + binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.action_add) { + addToLibrary(manga) + } + } + } + destroyActionModeIfNeeded() + } + + private fun showDeleteChaptersConfirmationDialog() { + DeleteChaptersDialog(this).showDialog(router) + } + + override fun deleteChapters() { + deleteChapters(getSelectedChapters()) + } + + private fun markPreviousAsRead(chapters: List) { + val adapter = chaptersAdapter ?: return + val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items + val chapterPos = prevChapters.indexOf(chapters.last()) + if (chapterPos != -1) { + markAsRead(prevChapters.take(chapterPos)) + } + destroyActionModeIfNeeded() + } + + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + presenter.bookmarkChapters(chapters, bookmarked) + destroyActionModeIfNeeded() + } + + fun deleteChapters(chapters: List) { + if (chapters.isEmpty()) return + + presenter.deleteChapters(chapters) + destroyActionModeIfNeeded() + } + + fun onChaptersDeleted(chapters: List) { + // this is needed so the downloaded text gets removed from the item + chapters.forEach { + chaptersAdapter?.updateItem(it) + } + chaptersAdapter?.notifyDataSetChanged() + } + + fun onChaptersDeletedError(error: Throwable) { + Timber.e(error) + } + + // OVERFLOW MENU DIALOGS + + private fun setDisplayMode(id: Int) { + presenter.setDisplayMode(id) + chaptersAdapter?.notifyDataSetChanged() + } + + private fun getUnreadChaptersSorted() = presenter.chapters + .filter { !it.read && it.status == Download.NOT_DOWNLOADED } + .distinctBy { it.name } + .sortedByDescending { it.source_order } + + private fun downloadChapters(choice: Int) { + val chaptersToDownload = when (choice) { + R.id.download_next -> getUnreadChaptersSorted().take(1) + R.id.download_next_5 -> getUnreadChaptersSorted().take(5) + R.id.download_next_10 -> getUnreadChaptersSorted().take(10) + R.id.download_custom -> { + showCustomDownloadDialog() + return + } + R.id.download_unread -> presenter.chapters.filter { !it.read } + R.id.download_all -> presenter.chapters + else -> emptyList() + } + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + destroyActionModeIfNeeded() + } + + private fun showCustomDownloadDialog() { + DownloadCustomChaptersDialog( + this, + presenter.chapters.size + ).showDialog(router) + } + + override fun downloadCustomChapters(amount: Int) { + val chaptersToDownload = getUnreadChaptersSorted().take(amount) + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + } + + // Chapters list - end + companion object { const val FROM_SOURCE_EXTRA = "from_source" const val MANGA_EXTRA = "manga" - - const val INFO_CHAPTERS_CONTROLLER = 0 - const val TRACK_CONTROLLER = 1 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 94c0c971fa..fd0e173389 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.manga.chapter +package eu.kanade.tachiyomi.ui.manga import android.os.Bundle import com.jakewharton.rxrelay.PublishRelay @@ -13,6 +13,7 @@ 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.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed @@ -29,14 +30,14 @@ import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class MangaInfoChaptersPresenter( +class MangaPresenter( val manga: Manga, val source: Source, val preferences: PreferencesHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val coverCache: CoverCache = Injekt.get() -) : BasePresenter() { +) : BasePresenter() { /** * Subscription to update the manga from the source. @@ -83,7 +84,7 @@ class MangaInfoChaptersPresenter( // Prepare the relay. chaptersRelay.flatMap { applyChapterFilters(it) } .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(MangaInfoChaptersController::onNextChapters) { _, error -> Timber.e(error) } + .subscribeLatestCache(MangaController::onNextChapters) { _, error -> Timber.e(error) } // Manga info - end @@ -139,7 +140,7 @@ class MangaInfoChaptersPresenter( { view, _ -> view.onFetchMangaInfoDone() }, - MangaInfoChaptersController::onFetchMangaInfoError + MangaController::onFetchMangaInfoError ) } @@ -226,7 +227,7 @@ class MangaInfoChaptersPresenter( .observeOn(AndroidSchedulers.mainThread()) .filter { download -> download.manga.id == manga.id } .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(MangaInfoChaptersController::onChapterStatusChange) { _, error -> + .subscribeLatestCache(MangaController::onChapterStatusChange) { _, error -> Timber.e(error) } } @@ -279,7 +280,7 @@ class MangaInfoChaptersPresenter( { view, _ -> view.onFetchChaptersDone() }, - MangaInfoChaptersController::onFetchChaptersError + MangaController::onFetchChaptersError ) } @@ -413,7 +414,7 @@ class MangaInfoChaptersPresenter( { view, _ -> view.onChaptersDeleted(chapters) }, - MangaInfoChaptersController::onChaptersDeletedError + MangaController::onChaptersDeletedError ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt index 0275502c08..9ead9cde9e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt @@ -4,6 +4,7 @@ import android.content.Context import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.getResourceColor import java.text.DateFormat import java.text.DecimalFormat @@ -11,7 +12,7 @@ import java.text.DecimalFormatSymbols import uy.kohesive.injekt.injectLazy class ChaptersAdapter( - controller: MangaInfoChaptersController, + controller: MangaController, context: Context ) : FlexibleAdapter(null, controller, true) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersController.kt deleted file mode 100644 index 364523295c..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersController.kt +++ /dev/null @@ -1,859 +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.ConcatAdapter -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.Category -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.ChaptersControllerBinding -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.ui.recent.history.HistoryController -import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController -import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.system.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 kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.swiperefreshlayout.refreshes -import timber.log.Timber -import uy.kohesive.injekt.injectLazy - -class MangaInfoChaptersController(private val fromSource: Boolean = false) : - NucleusController(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - ChangeMangaCategoriesDialog.Listener, - DownloadCustomChaptersDialog.Listener, - DeleteChaptersDialog.Listener { - - private val preferences: PreferencesHelper by injectLazy() - - private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null - private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null - private var chaptersAdapter: ChaptersAdapter? = null - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - /** - * Selected items. Used to restore selections after a rotation. - */ - private val selectedChapters = mutableSetOf() - - private val isLocalSource by lazy { presenter.source.id == LocalSource.ID } - - private var lastClickPosition = -1 - - private var isRefreshingInfo = false - private var isRefreshingChapters = false - - init { - setHasOptionsMenu(true) - setOptionsMenuHidden(true) - } - - override fun createPresenter(): MangaInfoChaptersPresenter { - val ctrl = parentController as MangaController - return MangaInfoChaptersPresenter( - ctrl.manga!!, ctrl.source!! - ) - } - - 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 - mangaInfoAdapter = MangaInfoHeaderAdapter(this, fromSource) - chaptersHeaderAdapter = MangaChaptersHeaderAdapter() - chaptersAdapter = ChaptersAdapter(this, view.context) - - binding.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - binding.recycler.setHasFixedSize(true) - chaptersAdapter?.fastScroller = binding.fastScroller - - // Skips directly to chapters list if navigated to from the library - binding.recycler.post { - if (!fromSource && preferences.jumpToChapters()) { - (binding.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0) - } - } - - binding.swipeRefresh.refreshes() - .onEach { - fetchMangaInfoFromSource(manualFetch = true) - 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() - mangaInfoAdapter = null - chaptersHeaderAdapter = null - chaptersAdapter = null - super.onDestroyView(view) - } - - override fun onActivityResumed(activity: Activity) { - if (view == null) return - - // Check if animation view is visible - if (binding.revealView.visibility == View.VISIBLE) { - // Show the unreveal effect - 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 - - // Display mode submenu - if (presenter.manga.displayMode == Manga.DISPLAY_NAME) { - menu.findItem(R.id.display_title).isChecked = true - } else { - menu.findItem(R.id.display_chapter_number).isChecked = true - } - - // Sorting mode submenu - val sortingItem = when (presenter.manga.sorting) { - Manga.SORTING_SOURCE -> R.id.sort_by_source - Manga.SORTING_NUMBER -> R.id.sort_by_number - Manga.SORTING_UPLOAD_DATE -> R.id.sort_by_upload_date - else -> throw NotImplementedError("Unimplemented sorting method") - } - menu.findItem(sortingItem).isChecked = true - menu.findItem(R.id.action_sort_descending).isChecked = presenter.manga.sortDescending() - - // Hide download options for local manga - menu.findItem(R.id.download_group).isVisible = !isLocalSource - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.display_title -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NAME) - } - R.id.display_chapter_number -> { - item.isChecked = true - setDisplayMode(Manga.DISPLAY_NUMBER) - } - - R.id.sort_by_source -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_SOURCE) - } - R.id.sort_by_number -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_NUMBER) - } - R.id.sort_by_upload_date -> { - item.isChecked = true - presenter.setSorting(Manga.SORTING_UPLOAD_DATE) - } - R.id.action_sort_descending -> { - presenter.reverseSortOrder() - activity?.invalidateOptionsMenu() - } - - R.id.download_next, R.id.download_next_5, R.id.download_next_10, - R.id.download_custom, R.id.download_unread, R.id.download_all - -> downloadChapters(item.itemId) - - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - activity?.invalidateOptionsMenu() - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity?.invalidateOptionsMenu() - } - - R.id.action_migrate -> migrateManga() - } - return super.onOptionsItemSelected(item) - } - - private fun updateRefreshing() { - binding.swipeRefresh.isRefreshing = isRefreshingInfo || isRefreshingChapters - } - - // Manga info - start - - /** - * Check if manga is initialized. - * If true update header with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextMangaInfo(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - mangaInfoAdapter?.update(manga, source) - } else { - // Initialize manga. - fetchMangaInfoFromSource() - } - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaInfoFromSource(manualFetch: Boolean = false) { - isRefreshingInfo = true - updateRefreshing() - - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource(manualFetch) - } - - fun onFetchMangaInfoDone() { - isRefreshingInfo = false - updateRefreshing() - } - - fun onFetchMangaInfoError(error: Throwable) { - isRefreshingInfo = false - updateRefreshing() - activity?.toast(error.message) - } - - fun openMangaInWebView() { - val source = presenter.source as? HttpSource ?: return - - val url = try { - source.mangaDetailsRequest(presenter.manga).url.toString() - } catch (e: Exception) { - return - } - - val activity = activity ?: return - val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title) - startActivity(intent) - } - - fun shareManga() { - val context = view?.context ?: return - - val source = presenter.source as? HttpSource ?: return - try { - val url = source.mangaDetailsRequest(presenter.manga).url.toString() - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - fun onFavoriteClick() { - val manga = presenter.manga - - if (manga.favorite) { - toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_removed_library)) - } else { - addToLibrary(manga) - } - } - - private fun addToLibrary(manga: Manga) { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - - when { - // Default category set - defaultCategory != null -> { - toggleFavorite() - presenter.moveMangaToCategory(manga, defaultCategory) - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - // Automatic 'Default' or no categories - defaultCategoryId == 0 || categories.isEmpty() -> { - toggleFavorite() - presenter.moveMangaToCategory(manga, null) - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - // Choose a category - else -> { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - } - } - - /** - * 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() - } - } - } - - mangaInfoAdapter?.notifyDataSetChanged() - } - - fun onCategoriesClick() { - val manga = presenter.manga - val categories = presenter.getCategories() - - val ids = presenter.getMangaCategoryIds(manga) - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - - override fun updateCategoriesForMangas(mangas: List, categories: List) { - val manga = mangas.firstOrNull() ?: return - - if (!manga.favorite) { - toggleFavorite() - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - presenter.moveMangaToCategories(manga, categories) - } - - /** - * Perform a global search using the provided query. - * - * @param query the search query to pass to the search controller - */ - fun performGlobalSearch(query: String) { - val router = parentController?.router ?: return - router.pushController(GlobalSearchController(query).withFadeTransaction()) - } - - /** - * Perform a search using the provided query. - * - * @param query the search query to the parent controller - */ - fun performSearch(query: String) { - 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) - } - } - } - - // Manga info - end - - // Chapters list - start - - /** - * Initiates source migration for the specific manga. - */ - private fun migrateManga() { - val controller = - SearchController( - presenter.manga - ) - controller.targetController = this - parentController!!.router.pushController(controller.withFadeTransaction()) - } - - fun onNextChapters(chapters: List) { - // If the list is empty and it hasn't requested previously, fetch chapters from source - // We use presenter chapters instead because they are always unfiltered - if (!presenter.hasRequested && presenter.chapters.isEmpty()) { - fetchChaptersFromSource() - } - - val chaptersHeader = chaptersHeaderAdapter ?: return - chaptersHeader.setNumChapters(chapters.size) - - val adapter = chaptersAdapter ?: return - adapter.updateDataSet(chapters) - - if (selectedChapters.isNotEmpty()) { - adapter.clearSelection() // we need to start from a clean state, index may have changed - createActionModeIfNeeded() - selectedChapters.forEach { item -> - val position = adapter.indexOf(item) - if (position != -1 && !adapter.isSelected(position)) { - adapter.toggleSelection(position) - } - } - actionMode?.invalidate() - } - - val context = view?.context - if (context != null && chapters.any { it.read }) { - binding.fab.text = context.getString(R.string.action_resume) - } - } - - private fun fetchChaptersFromSource(manualFetch: Boolean = false) { - isRefreshingChapters = true - updateRefreshing() - - presenter.fetchChaptersFromSource(manualFetch) - } - - fun onFetchChaptersDone() { - isRefreshingChapters = false - updateRefreshing() - } - - fun onFetchChaptersError(error: Throwable) { - isRefreshingChapters = false - updateRefreshing() - activity?.toast(error.message) - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder - } - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - override fun onItemClick(view: View?, position: Int): Boolean { - val adapter = chaptersAdapter ?: return false - val item = adapter.getItem(position) ?: return false - return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) { - lastClickPosition = position - toggleSelection(position) - true - } else { - openChapter(item.chapter) - false - } - } - - override fun onItemLongClick(position: Int) { - createActionModeIfNeeded() - when { - lastClickPosition == -1 -> setSelection(position) - lastClickPosition > position -> - for (i in position until lastClickPosition) - setSelection(i) - lastClickPosition < position -> - for (i in lastClickPosition + 1..position) - setSelection(i) - else -> setSelection(position) - } - lastClickPosition = position - chaptersAdapter?.notifyDataSetChanged() - } - - // SELECTIONS & ACTION MODE - - private fun toggleSelection(position: Int) { - val adapter = chaptersAdapter ?: return - val item = adapter.getItem(position) ?: return - adapter.toggleSelection(position) - adapter.notifyDataSetChanged() - if (adapter.isSelected(position)) { - selectedChapters.add(item) - } else { - selectedChapters.remove(item) - } - actionMode?.invalidate() - } - - private fun setSelection(position: Int) { - val adapter = chaptersAdapter ?: return - val item = adapter.getItem(position) ?: return - if (!adapter.isSelected(position)) { - adapter.toggleSelection(position) - selectedChapters.add(item) - actionMode?.invalidate() - } - } - - private fun getSelectedChapters(): List { - val adapter = chaptersAdapter ?: return emptyList() - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } - } - - private fun createActionModeIfNeeded() { - if (actionMode == null) { - actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) - binding.actionToolbar.show( - actionMode!!, - R.menu.chapter_selection - ) { onActionItemClicked(it!!) } - } - } - - private fun destroyActionModeIfNeeded() { - lastClickPosition = -1 - actionMode?.finish() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.generic_selection, menu) - chaptersAdapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = chaptersAdapter?.selectedItemCount ?: 0 - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = count.toString() - - val chapters = getSelectedChapters() - binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && chapters.any { !it.isDownloaded } - binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && chapters.any { it.isDownloaded } - binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } - binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } - binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } - binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } - - // Hide FAB to avoid interfering with the bottom action toolbar - // 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() - chaptersAdapter?.mode = SelectableAdapter.Mode.SINGLE - chaptersAdapter?.clearSelection() - selectedChapters.clear() - actionMode = null - - // TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton] - // fails to show up properly - // binding.fab.show() - binding.fab.visible() - } - - override fun onDetach(view: View) { - destroyActionModeIfNeeded() - super.onDetach(view) - } - - // SELECTION MODE ACTIONS - - private fun selectAll() { - val adapter = chaptersAdapter ?: return - adapter.selectAll() - selectedChapters.addAll(adapter.items) - actionMode?.invalidate() - } - - private fun selectInverse() { - val adapter = chaptersAdapter ?: return - - selectedChapters.clear() - for (i in 0..adapter.itemCount) { - adapter.toggleSelection(i) - } - selectedChapters.addAll(adapter.selectedPositions.mapNotNull { adapter.getItem(it) }) - - actionMode?.invalidate() - adapter.notifyDataSetChanged() - } - - private fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - destroyActionModeIfNeeded() - } - - private fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - destroyActionModeIfNeeded() - } - - private fun downloadChapters(chapters: List) { - val view = view - val manga = presenter.manga - presenter.downloadChapters(chapters) - if (view != null && !manga.favorite) { - binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - addToLibrary(manga) - } - } - } - destroyActionModeIfNeeded() - } - - private fun showDeleteChaptersConfirmationDialog() { - DeleteChaptersDialog(this).showDialog(router) - } - - override fun deleteChapters() { - deleteChapters(getSelectedChapters()) - } - - private fun markPreviousAsRead(chapters: List) { - val adapter = chaptersAdapter ?: return - val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = prevChapters.indexOf(chapters.last()) - if (chapterPos != -1) { - markAsRead(prevChapters.take(chapterPos)) - } - destroyActionModeIfNeeded() - } - - private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - presenter.bookmarkChapters(chapters, bookmarked) - destroyActionModeIfNeeded() - } - - fun deleteChapters(chapters: List) { - if (chapters.isEmpty()) return - - presenter.deleteChapters(chapters) - destroyActionModeIfNeeded() - } - - fun onChaptersDeleted(chapters: List) { - // this is needed so the downloaded text gets removed from the item - chapters.forEach { - chaptersAdapter?.updateItem(it) - } - chaptersAdapter?.notifyDataSetChanged() - } - - fun onChaptersDeletedError(error: Throwable) { - Timber.e(error) - } - - // OVERFLOW MENU DIALOGS - - private fun setDisplayMode(id: Int) { - presenter.setDisplayMode(id) - chaptersAdapter?.notifyDataSetChanged() - } - - private fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - private fun downloadChapters(choice: Int) { - val chaptersToDownload = when (choice) { - R.id.download_next -> getUnreadChaptersSorted().take(1) - R.id.download_next_5 -> getUnreadChaptersSorted().take(5) - R.id.download_next_10 -> getUnreadChaptersSorted().take(10) - R.id.download_custom -> { - showCustomDownloadDialog() - return - } - R.id.download_unread -> presenter.chapters.filter { !it.read } - R.id.download_all -> presenter.chapters - else -> emptyList() - } - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - destroyActionModeIfNeeded() - } - - private fun showCustomDownloadDialog() { - DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router) - } - - override fun downloadCustomChapters(amount: Int) { - val chaptersToDownload = getUnreadChaptersSorted().take(amount) - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } - - // Chapters list - end -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaCoverImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaCoverImageView.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt index 988a98e8ff..f7e5daf1cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaCoverImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.manga.chapter +package eu.kanade.tachiyomi.ui.manga.info import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoHeaderAdapter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 52fb56a308..6a3d7eeae8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.manga.chapter +package eu.kanade.tachiyomi.ui.manga.info import android.content.Context import android.text.TextUtils @@ -13,11 +13,13 @@ 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.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.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.setChips @@ -35,7 +37,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class MangaInfoHeaderAdapter( - private val controller: MangaInfoChaptersController, + private val controller: MangaController, private val fromSource: Boolean ) : RecyclerView.Adapter() { @@ -81,13 +83,24 @@ class MangaInfoHeaderAdapter( .onEach { controller.onFavoriteClick() } .launchIn(scope) + if (controller.presenter.manga.favorite && Injekt.get().hasLoggedServices()) { + binding.btnTracking.visible() + binding.btnTracking.clicks() + .onEach { controller.onTrackingClick() } + .launchIn(scope) + } else { + binding.btnTracking.gone() + } + if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) { binding.btnCategories.visible() + binding.btnCategories.clicks() + .onEach { controller.onCategoriesClick() } + .launchIn(scope) + binding.btnCategories.setTooltip(R.string.action_move_category) + } else { + binding.btnCategories.gone() } - binding.btnCategories.clicks() - .onEach { controller.onCategoriesClick() } - .launchIn(scope) - binding.btnCategories.setTooltip(R.string.action_move_category) if (controller.presenter.source is HttpSource) { binding.btnWebview.visible() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt index 09778f6ea7..4597df4f49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt @@ -2,29 +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.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 : - NucleusController(), + NucleusController, TrackAdapter.OnClickListener, SetTrackStatusDialog.Listener, SetTrackChaptersDialog.Listener, SetTrackScoreDialog.Listener, SetTrackReadingDatesDialog.Listener { + constructor(manga: Manga?) : super( + Bundle().apply { + putLong(MANGA_EXTRA, manga?.id ?: 0) + } + ) { + this.manga = manga + } + + constructor(mangaId: Long) : this( + Injekt.get().getManga(mangaId).executeAsBlocking() + ) + + @Suppress("unused") + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) + + var manga: Manga? = null + private set + private var adapter: TrackAdapter? = null init { @@ -33,8 +55,12 @@ class TrackController : setHasOptionsMenu(true) } + override fun getTitle(): String? { + return manga?.title + } + override fun createPresenter(): TrackPresenter { - return TrackPresenter((parentController as MangaController).manga!!) + return TrackPresenter(manga!!) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -45,6 +71,8 @@ class TrackController : 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 @@ -63,7 +91,6 @@ class TrackController : val atLeastOneLink = trackings.any { it.track != null } adapter?.items = trackings binding.swipeRefresh.isEnabled = atLeastOneLink - (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) } fun onSearchResults(results: List) { @@ -167,6 +194,7 @@ class TrackController : } private companion object { + const val MANGA_EXTRA = "manga" const val TAG_SEARCH_CONTROLLER = "track_search_controller" } } diff --git a/app/src/main/res/layout/manga_info_header.xml b/app/src/main/res/layout/manga_info_header.xml index 5f118a2414..4930c0a7de 100644 --- a/app/src/main/res/layout/manga_info_header.xml +++ b/app/src/main/res/layout/manga_info_header.xml @@ -32,17 +32,17 @@ android:id="@+id/manga_info" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="16dp" android:orientation="horizontal" + android:padding="16dp" app:layout_constraintTop_toTopOf="parent"> - + +