mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 01:51:51 +01:00
Move tracking to manga info actions
Currently just opens a separate view. To be iterated upon later.
This commit is contained in:
parent
fa5d2276c0
commit
23fe848a35
@ -75,7 +75,6 @@ open class GlobalSearchController(
|
|||||||
* @param manga clicked item containing manga information.
|
* @param manga clicked item containing manga information.
|
||||||
*/
|
*/
|
||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
// Open MangaController.
|
|
||||||
router.pushController(MangaController(manga, true).withFadeTransaction())
|
router.pushController(MangaController(manga, true).withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,37 +1,84 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga
|
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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.databinding.PagerControllerBinding
|
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.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.MangaInfoChaptersController
|
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.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 eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
import eu.kanade.tachiyomi.util.view.getCoordinates
|
||||||
import rx.Subscription
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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(
|
constructor(manga: Manga?, fromSource: Boolean = false) : super(
|
||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
@ -58,20 +105,48 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
var source: Source? = null
|
var source: Source? = null
|
||||||
private set
|
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? {
|
override fun getTitle(): String? {
|
||||||
return manga?.title
|
return manga?.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createPresenter(): MangaPresenter {
|
||||||
|
return MangaPresenter(
|
||||||
|
manga!!,
|
||||||
|
source!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
binding = PagerControllerBinding.inflate(inflater)
|
binding = ChaptersControllerBinding.inflate(inflater)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,23 +155,65 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
|
|
||||||
if (manga == null || source == null) return
|
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.recycler.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter)
|
||||||
binding.pager.adapter = adapter
|
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) {
|
// Skips directly to chapters list if navigated to from the library
|
||||||
adapter = null
|
binding.recycler.post {
|
||||||
super.onDestroyView(view)
|
if (!fromSource && preferences.jumpToChapters()) {
|
||||||
}
|
(binding.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0)
|
||||||
|
}
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
|
||||||
super.onChangeStarted(handler, type)
|
|
||||||
if (type.isEnter) {
|
|
||||||
activity?.tabs?.setupWithViewPager(binding.pager)
|
|
||||||
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
@ -107,68 +224,704 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun configureTabs(tabs: TabLayout) {
|
override fun onDestroyView(view: View) {
|
||||||
with(tabs) {
|
destroyActionModeIfNeeded()
|
||||||
tabGravity = TabLayout.GRAVITY_FILL
|
binding.actionToolbar.destroy()
|
||||||
tabMode = TabLayout.MODE_FIXED
|
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) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
trackingIconSubscription?.unsubscribe()
|
inflater.inflate(R.menu.chapters, menu)
|
||||||
setTrackingIconInternal(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTrackingIcon(visible: Boolean) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
trackingIconRelay.call(visible)
|
// 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) {
|
// Set correct checkbox values.
|
||||||
val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
|
menuFilterRead.isChecked = presenter.onlyRead()
|
||||||
val drawable = if (visible) {
|
menuFilterUnread.isChecked = presenter.onlyUnread()
|
||||||
VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
|
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 {
|
} 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.id.sort_by_source -> {
|
||||||
R.string.manga_chapters_tab,
|
item.isChecked = true
|
||||||
R.string.manga_tracking_tab
|
presenter.setSorting(Manga.SORTING_SOURCE)
|
||||||
)
|
}
|
||||||
.map { resources!!.getString(it) }
|
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<TrackManager>().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 {
|
R.id.action_filter_unread -> {
|
||||||
return tabCount
|
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) {
|
val activity = activity ?: return
|
||||||
if (!router.hasRootController()) {
|
val intent = WebViewActivity.newIntent(activity, url, source.id, presenter.manga.title)
|
||||||
val controller = when (position) {
|
startActivity(intent)
|
||||||
INFO_CHAPTERS_CONTROLLER -> MangaInfoChaptersController(fromSource)
|
}
|
||||||
TRACK_CONTROLLER -> TrackController()
|
|
||||||
else -> error("Wrong position $position")
|
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 {
|
mangaInfoAdapter?.notifyDataSetChanged()
|
||||||
return tabTitles[position]
|
}
|
||||||
|
|
||||||
|
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 {
|
companion object {
|
||||||
const val FROM_SOURCE_EXTRA = "from_source"
|
const val FROM_SOURCE_EXTRA = "from_source"
|
||||||
const val MANGA_EXTRA = "manga"
|
const val MANGA_EXTRA = "manga"
|
||||||
|
|
||||||
const val INFO_CHAPTERS_CONTROLLER = 0
|
|
||||||
const val TRACK_CONTROLLER = 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
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.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
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.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||||
@ -29,14 +30,14 @@ import timber.log.Timber
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class MangaInfoChaptersPresenter(
|
class MangaPresenter(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val source: Source,
|
val source: Source,
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
private val db: DatabaseHelper = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get()
|
private val coverCache: CoverCache = Injekt.get()
|
||||||
) : BasePresenter<MangaInfoChaptersController>() {
|
) : BasePresenter<MangaController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to update the manga from the source.
|
* Subscription to update the manga from the source.
|
||||||
@ -83,7 +84,7 @@ class MangaInfoChaptersPresenter(
|
|||||||
// Prepare the relay.
|
// Prepare the relay.
|
||||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeLatestCache(MangaInfoChaptersController::onNextChapters) { _, error -> Timber.e(error) }
|
.subscribeLatestCache(MangaController::onNextChapters) { _, error -> Timber.e(error) }
|
||||||
|
|
||||||
// Manga info - end
|
// Manga info - end
|
||||||
|
|
||||||
@ -139,7 +140,7 @@ class MangaInfoChaptersPresenter(
|
|||||||
{ view, _ ->
|
{ view, _ ->
|
||||||
view.onFetchMangaInfoDone()
|
view.onFetchMangaInfoDone()
|
||||||
},
|
},
|
||||||
MangaInfoChaptersController::onFetchMangaInfoError
|
MangaController::onFetchMangaInfoError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +227,7 @@ class MangaInfoChaptersPresenter(
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.filter { download -> download.manga.id == manga.id }
|
.filter { download -> download.manga.id == manga.id }
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
.doOnNext { onDownloadStatusChange(it) }
|
||||||
.subscribeLatestCache(MangaInfoChaptersController::onChapterStatusChange) { _, error ->
|
.subscribeLatestCache(MangaController::onChapterStatusChange) { _, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,7 +280,7 @@ class MangaInfoChaptersPresenter(
|
|||||||
{ view, _ ->
|
{ view, _ ->
|
||||||
view.onFetchChaptersDone()
|
view.onFetchChaptersDone()
|
||||||
},
|
},
|
||||||
MangaInfoChaptersController::onFetchChaptersError
|
MangaController::onFetchChaptersError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,7 +414,7 @@ class MangaInfoChaptersPresenter(
|
|||||||
{ view, _ ->
|
{ view, _ ->
|
||||||
view.onChaptersDeleted(chapters)
|
view.onChaptersDeleted(chapters)
|
||||||
},
|
},
|
||||||
MangaInfoChaptersController::onChaptersDeletedError
|
MangaController::onChaptersDeletedError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
@ -11,7 +12,7 @@ import java.text.DecimalFormatSymbols
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ChaptersAdapter(
|
class ChaptersAdapter(
|
||||||
controller: MangaInfoChaptersController,
|
controller: MangaController,
|
||||||
context: Context
|
context: Context
|
||||||
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
||||||
|
|
||||||
|
@ -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,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga.info
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga.info
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
|
import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
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.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
import eu.kanade.tachiyomi.util.view.gone
|
||||||
import eu.kanade.tachiyomi.util.view.setChips
|
import eu.kanade.tachiyomi.util.view.setChips
|
||||||
@ -35,7 +37,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class MangaInfoHeaderAdapter(
|
class MangaInfoHeaderAdapter(
|
||||||
private val controller: MangaInfoChaptersController,
|
private val controller: MangaController,
|
||||||
private val fromSource: Boolean
|
private val fromSource: Boolean
|
||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
||||||
@ -81,13 +83,24 @@ class MangaInfoHeaderAdapter(
|
|||||||
.onEach { controller.onFavoriteClick() }
|
.onEach { controller.onFavoriteClick() }
|
||||||
.launchIn(scope)
|
.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()) {
|
if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) {
|
||||||
binding.btnCategories.visible()
|
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) {
|
if (controller.presenter.source is HttpSource) {
|
||||||
binding.btnWebview.visible()
|
binding.btnWebview.visible()
|
@ -2,29 +2,51 @@ package eu.kanade.tachiyomi.ui.manga.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
|
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
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.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class TrackController :
|
class TrackController :
|
||||||
NucleusController<TrackControllerBinding, TrackPresenter>(),
|
NucleusController<TrackControllerBinding, TrackPresenter>,
|
||||||
TrackAdapter.OnClickListener,
|
TrackAdapter.OnClickListener,
|
||||||
SetTrackStatusDialog.Listener,
|
SetTrackStatusDialog.Listener,
|
||||||
SetTrackChaptersDialog.Listener,
|
SetTrackChaptersDialog.Listener,
|
||||||
SetTrackScoreDialog.Listener,
|
SetTrackScoreDialog.Listener,
|
||||||
SetTrackReadingDatesDialog.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
|
private var adapter: TrackAdapter? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -33,8 +55,12 @@ class TrackController :
|
|||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return manga?.title
|
||||||
|
}
|
||||||
|
|
||||||
override fun createPresenter(): TrackPresenter {
|
override fun createPresenter(): TrackPresenter {
|
||||||
return TrackPresenter((parentController as MangaController).manga!!)
|
return TrackPresenter(manga!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
@ -45,6 +71,8 @@ class TrackController :
|
|||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
if (manga == null) return
|
||||||
|
|
||||||
adapter = TrackAdapter(this)
|
adapter = TrackAdapter(this)
|
||||||
binding.trackRecycler.layoutManager = LinearLayoutManager(view.context)
|
binding.trackRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.trackRecycler.adapter = adapter
|
binding.trackRecycler.adapter = adapter
|
||||||
@ -63,7 +91,6 @@ class TrackController :
|
|||||||
val atLeastOneLink = trackings.any { it.track != null }
|
val atLeastOneLink = trackings.any { it.track != null }
|
||||||
adapter?.items = trackings
|
adapter?.items = trackings
|
||||||
binding.swipeRefresh.isEnabled = atLeastOneLink
|
binding.swipeRefresh.isEnabled = atLeastOneLink
|
||||||
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSearchResults(results: List<TrackSearch>) {
|
fun onSearchResults(results: List<TrackSearch>) {
|
||||||
@ -167,6 +194,7 @@ class TrackController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
const val MANGA_EXTRA = "manga"
|
||||||
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
|
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,17 +32,17 @@
|
|||||||
android:id="@+id/manga_info"
|
android:id="@+id/manga_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="16dp"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
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:id="@+id/manga_cover"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:maxWidth="220dp"
|
|
||||||
android:background="@drawable/rounded_rectangle"
|
android:background="@drawable/rounded_rectangle"
|
||||||
android:contentDescription="@string/description_cover"
|
android:contentDescription="@string/description_cover"
|
||||||
|
android:maxWidth="220dp"
|
||||||
tools:src="@mipmap/ic_launcher" />
|
tools:src="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -126,6 +126,17 @@
|
|||||||
android:text="@string/add_to_library"
|
android:text="@string/add_to_library"
|
||||||
app:icon="@drawable/ic_favorite_border_24dp" />
|
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
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn_categories"
|
android:id="@+id/btn_categories"
|
||||||
style="@style/Theme.Widget.Button.Icon.Textless"
|
style="@style/Theme.Widget.Button.Icon.Textless"
|
||||||
|
Loading…
Reference in New Issue
Block a user