瀏覽代碼

Move tracking to manga info actions

Currently just opens a separate view. To be iterated upon later.
arkon 4 年之前
父節點
當前提交
23fe848a35

+ 0 - 1
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())
     }
 

+ 824 - 71
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<PagerControllerBinding>, TabbedController {
+class MangaController :
+    NucleusController<ChaptersControllerBinding, MangaPresenter>,
+    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<PagerControllerBinding>, 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<Boolean> = 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<ChapterItem>()
+
+    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<PagerControllerBinding>, TabbedController {
 
         if (manga == null || source == null) return
 
-        requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
-
-        adapter = MangaDetailAdapter()
-        binding.pager.adapter = adapter
-    }
+        // Init RecyclerView and adapter
+        mangaInfoAdapter =
+            MangaInfoHeaderAdapter(
+                this,
+                fromSource
+            )
+        chaptersHeaderAdapter =
+            MangaChaptersHeaderAdapter()
+        chaptersAdapter = ChaptersAdapter(
+            this,
+            view.context
+        )
 
-    override fun onDestroyView(view: View) {
-        adapter = null
-        super.onDestroyView(view)
-    }
+        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 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<PagerControllerBinding>, 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)
+
+        // 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
     }
 
-    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)
+    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 {
-            null
+            // Initialize manga.
+            fetchMangaInfoFromSource()
         }
+    }
 
-        tab.icon = drawable
+    /**
+     * 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)
     }
 
-    private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
+    fun onFetchMangaInfoDone() {
+        isRefreshingInfo = false
+        updateRefreshing()
+    }
 
-        private val tabTitles = listOf(
-            R.string.manga_chapters_tab,
-            R.string.manga_tracking_tab
-        )
-            .map { resources!!.getString(it) }
+    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)
+        }
+    }
+
+    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))
+            }
 
-        private val tabCount = tabTitles.size - if (Injekt.get<TrackManager>().hasLoggedServices()) 0 else 1
+            // Choose a category
+            else -> {
+                val ids = presenter.getMangaCategoryIds(manga)
+                val preselected = ids.mapNotNull { id ->
+                    categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
+                }.toTypedArray()
 
-        override fun getCount(): Int {
-            return tabCount
+                ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
+                    .showDialog(router)
+            }
         }
+    }
 
-        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")
+    /**
+     * 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<Manga>, categories: List<Category>) {
+        val manga = mangas.firstOrNull() ?: return
+
+        if (!manga.favorite) {
+            toggleFavorite()
+            activity?.toast(activity?.getString(R.string.manga_added_library))
         }
+
+        presenter.moveMangaToCategories(manga, categories)
     }
 
+    /**
+     * Perform a global search using the provided query.
+     *
+     * @param query the search query to pass to the search controller
+     */
+    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<ChapterItem>) {
+        // If the list is empty and it hasn't requested previously, fetch chapters from source
+        // We use presenter chapters instead because they are always unfiltered
+        if (!presenter.hasRequested && presenter.chapters.isEmpty()) {
+            fetchChaptersFromSource()
+        }
+
+        val 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<ChapterItem> {
+        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<ChapterItem>) {
+        presenter.markChaptersRead(chapters, true)
+        destroyActionModeIfNeeded()
+    }
+
+    private fun markAsUnread(chapters: List<ChapterItem>) {
+        presenter.markChaptersRead(chapters, false)
+        destroyActionModeIfNeeded()
+    }
+
+    private fun downloadChapters(chapters: List<ChapterItem>) {
+        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<ChapterItem>) {
+        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<ChapterItem>, bookmarked: Boolean) {
+        presenter.bookmarkChapters(chapters, bookmarked)
+        destroyActionModeIfNeeded()
+    }
+
+    fun deleteChapters(chapters: List<ChapterItem>) {
+        if (chapters.isEmpty()) return
+
+        presenter.deleteChapters(chapters)
+        destroyActionModeIfNeeded()
+    }
+
+    fun onChaptersDeleted(chapters: List<ChapterItem>) {
+        // this is needed so the downloaded text gets removed from the item
+        chapters.forEach {
+            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
     }
 }

+ 9 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersPresenter.kt → 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<MangaInfoChaptersController>() {
+) : BasePresenter<MangaController>() {
 
     /**
      * 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
             )
     }
 

+ 2 - 1
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<ChapterItem>(null, controller, true) {
 

+ 0 - 859
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoChaptersController.kt

@@ -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<ChaptersControllerBinding, MangaInfoChaptersPresenter>(),
-    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<ChapterItem>()
-
-    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<Manga>, categories: List<Category>) {
-        val manga = mangas.firstOrNull() ?: return
-
-        if (!manga.favorite) {
-            toggleFavorite()
-            activity?.toast(activity?.getString(R.string.manga_added_library))
-        }
-
-        presenter.moveMangaToCategories(manga, categories)
-    }
-
-    /**
-     * Perform a global search using the provided query.
-     *
-     * @param query the search query to pass to the search controller
-     */
-    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<ChapterItem>) {
-        // If the list is empty and it hasn't requested previously, fetch chapters from source
-        // We use presenter chapters instead because they are always unfiltered
-        if (!presenter.hasRequested && presenter.chapters.isEmpty()) {
-            fetchChaptersFromSource()
-        }
-
-        val 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<ChapterItem> {
-        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<ChapterItem>) {
-        presenter.markChaptersRead(chapters, true)
-        destroyActionModeIfNeeded()
-    }
-
-    private fun markAsUnread(chapters: List<ChapterItem>) {
-        presenter.markChaptersRead(chapters, false)
-        destroyActionModeIfNeeded()
-    }
-
-    private fun downloadChapters(chapters: List<ChapterItem>) {
-        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<ChapterItem>) {
-        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<ChapterItem>, bookmarked: Boolean) {
-        presenter.bookmarkChapters(chapters, bookmarked)
-        destroyActionModeIfNeeded()
-    }
-
-    fun deleteChapters(chapters: List<ChapterItem>) {
-        if (chapters.isEmpty()) return
-
-        presenter.deleteChapters(chapters)
-        destroyActionModeIfNeeded()
-    }
-
-    fun onChaptersDeleted(chapters: List<ChapterItem>) {
-        // this is needed so the downloaded text gets removed from the item
-        chapters.forEach {
-            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
-}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaCoverImageView.kt → 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

+ 19 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/MangaInfoHeaderAdapter.kt → 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<MangaInfoHeaderAdapter.HeaderViewHolder>() {
@@ -81,13 +83,24 @@ class MangaInfoHeaderAdapter(
                 .onEach { controller.onFavoriteClick() }
                 .launchIn(scope)
 
+            if (controller.presenter.manga.favorite && Injekt.get<TrackManager>().hasLoggedServices()) {
+                binding.btnTracking.visible()
+                binding.btnTracking.clicks()
+                    .onEach { controller.onTrackingClick() }
+                    .launchIn(scope)
+            } else {
+                binding.btnTracking.gone()
+            }
+
             if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) {
                 binding.btnCategories.visible()
+                binding.btnCategories.clicks()
+                    .onEach { controller.onCategoriesClick() }
+                    .launchIn(scope)
+                binding.btnCategories.setTooltip(R.string.action_move_category)
+            } else {
+                binding.btnCategories.gone()
             }
-            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()

+ 32 - 4
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<TrackControllerBinding, TrackPresenter>(),
+    NucleusController<TrackControllerBinding, TrackPresenter>,
     TrackAdapter.OnClickListener,
     SetTrackStatusDialog.Listener,
     SetTrackChaptersDialog.Listener,
     SetTrackScoreDialog.Listener,
     SetTrackReadingDatesDialog.Listener {
 
+    constructor(manga: Manga?) : super(
+        Bundle().apply {
+            putLong(MANGA_EXTRA, manga?.id ?: 0)
+        }
+    ) {
+        this.manga = manga
+    }
+
+    constructor(mangaId: Long) : this(
+        Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking()
+    )
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
+
+    var manga: Manga? = null
+        private set
+
     private var adapter: TrackAdapter? = null
 
     init {
@@ -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<TrackSearch>) {
@@ -167,6 +194,7 @@ class TrackController :
     }
 
     private companion object {
+        const val MANGA_EXTRA = "manga"
         const val TAG_SEARCH_CONTROLLER = "track_search_controller"
     }
 }

+ 14 - 3
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">
 
-            <eu.kanade.tachiyomi.ui.manga.chapter.MangaCoverImageView
+            <eu.kanade.tachiyomi.ui.manga.info.MangaCoverImageView
                 android:id="@+id/manga_cover"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:maxWidth="220dp"
                 android:background="@drawable/rounded_rectangle"
                 android:contentDescription="@string/description_cover"
+                android:maxWidth="220dp"
                 tools:src="@mipmap/ic_launcher" />
 
             <LinearLayout
@@ -126,6 +126,17 @@
                 android:text="@string/add_to_library"
                 app:icon="@drawable/ic_favorite_border_24dp" />
 
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/btn_tracking"
+                style="@style/Theme.Widget.Button.Icon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:text="@string/manga_tracking_tab"
+                android:visibility="gone"
+                app:icon="@drawable/ic_sync_24dp"
+                tools:visibility="visible" />
+
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/btn_categories"
                 style="@style/Theme.Widget.Button.Icon.Textless"