mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 20:19:05 +01:00
Migrate Updates screen to compose (#7534)
* Migrate Updates screen to compose * Review Changes + Cleanup Remove more unused stuff and show confirmation dialog when mass deleting chapters * Review Changes 2 + Rebase
This commit is contained in:
@@ -226,7 +226,7 @@ class MainActivity : BaseActivity() {
|
||||
if (!router.hasRootController()) {
|
||||
// Set start screen
|
||||
if (!handleIntentAction(intent)) {
|
||||
setSelectedNavItem(startScreenId)
|
||||
moveToStartScreen()
|
||||
}
|
||||
}
|
||||
syncActivityViewWithController()
|
||||
@@ -483,10 +483,15 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Updates screen has custom back handler
|
||||
if (router.getControllerWithTag("${R.id.nav_updates}") != null) {
|
||||
router.handleBack()
|
||||
return
|
||||
}
|
||||
val backstackSize = router.backstackSize
|
||||
if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
|
||||
// Return to start screen
|
||||
setSelectedNavItem(startScreenId)
|
||||
moveToStartScreen()
|
||||
} else if (shouldHandleExitConfirmation()) {
|
||||
// Exit confirmation (resets after 2 seconds)
|
||||
lifecycleScope.launchUI { resetExitConfirmation() }
|
||||
@@ -499,6 +504,10 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToStartScreen() {
|
||||
setSelectedNavItem(startScreenId)
|
||||
}
|
||||
|
||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
binding.appbar.apply {
|
||||
tag = isTransparentWhenNotLifted
|
||||
|
||||
@@ -27,7 +27,7 @@ import eu.kanade.data.chapter.NoChaptersException
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.presentation.manga.MangaScreen
|
||||
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
|
||||
|
||||
@@ -7,8 +7,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter.base
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.presentation.manga.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
|
||||
open class BaseChapterHolder(
|
||||
view: View,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
class DateSectionItem(
|
||||
private val date: Date,
|
||||
private val range: Int,
|
||||
private val dateFormat: DateFormat,
|
||||
) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.section_header_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
|
||||
return DateSectionItemHolder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: DateSectionItemHolder, position: Int, payloads: List<Any?>?) {
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is DateSectionItem) {
|
||||
return date == other.date
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return date.hashCode()
|
||||
}
|
||||
|
||||
inner class DateSectionItemHolder(private val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
|
||||
|
||||
private val binding = SectionHeaderItemBinding.bind(view)
|
||||
|
||||
fun bind(item: DateSectionItem) {
|
||||
binding.title.text = item.date.toRelativeString(view.context, range, dateFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ConfirmDeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : ConfirmDeleteChaptersDialog.Listener {
|
||||
|
||||
private var chaptersToDelete = emptyList<UpdatesItem>()
|
||||
|
||||
constructor(target: T, chaptersToDelete: List<UpdatesItem>) : this() {
|
||||
this.chaptersToDelete = chaptersToDelete
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.confirm_delete_chapters)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters(chaptersToDelete)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters(chaptersToDelete: List<UpdatesItem>)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.content.Context
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
class UpdatesAdapter(
|
||||
val controller: UpdatesController,
|
||||
context: Context,
|
||||
val items: List<IFlexible<*>>?,
|
||||
) : BaseChaptersAdapter<IFlexible<*>>(controller, items) {
|
||||
|
||||
var readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
|
||||
var unreadColor = context.getResourceColor(R.attr.colorOnSurface)
|
||||
val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
|
||||
var bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val coverClickListener: OnCoverClickListener = controller
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
interface OnCoverClickListener {
|
||||
fun onCoverClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,65 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.presentation.util.NavBarVisibility
|
||||
import eu.kanade.presentation.util.toBoolean
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.await
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||
|
||||
/**
|
||||
* Fragment that shows recent chapters.
|
||||
*/
|
||||
class UpdatesController :
|
||||
NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
|
||||
RootController,
|
||||
ActionModeWithToolbar.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
BaseChaptersAdapter.OnChapterClickListener,
|
||||
ConfirmDeleteChaptersDialog.Listener,
|
||||
UpdatesAdapter.OnCoverClickListener {
|
||||
FullComposeController<UpdatesPresenter>(),
|
||||
RootController {
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionModeWithToolbar? = null
|
||||
override fun createPresenter() = UpdatesPresenter()
|
||||
|
||||
/**
|
||||
* Adapter containing the recent chapters.
|
||||
*/
|
||||
var adapter: UpdatesAdapter? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_updates)
|
||||
}
|
||||
|
||||
override fun createPresenter(): UpdatesPresenter {
|
||||
return UpdatesPresenter()
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = UpdatesControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val state by presenter.state.collectAsState()
|
||||
when (state) {
|
||||
is UpdatesState.Loading -> LoadingScreen()
|
||||
is UpdatesState.Error -> Text(text = (state as UpdatesState.Error).error.message.orEmpty())
|
||||
is UpdatesState.Success ->
|
||||
UpdateScreen(
|
||||
state = (state as UpdatesState.Success),
|
||||
onClickCover = this::openManga,
|
||||
onClickUpdate = this::openChapter,
|
||||
onDownloadChapter = this::downloadChapters,
|
||||
onUpdateLibrary = this::updateLibrary,
|
||||
onBackClicked = this::onBackClicked,
|
||||
toggleNavBarVisibility = this::toggleNavBarVisibility,
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked = { updatesItems, bookmark ->
|
||||
presenter.bookmarkUpdates(updatesItems, bookmark)
|
||||
},
|
||||
onMultiMarkAsReadClicked = { updatesItems, read ->
|
||||
presenter.markUpdatesRead(updatesItems, read)
|
||||
},
|
||||
onMultiDeleteClicked = this::deleteChaptersWithConfirmation,
|
||||
)
|
||||
}
|
||||
|
||||
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
val layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.layoutManager = layoutManager
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
binding.recycler.scrollStateChanges()
|
||||
.onEach {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
binding.swipeRefresh.isEnabled = firstPos <= 0
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
binding.swipeRefresh.isRefreshing = true
|
||||
binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
|
||||
binding.swipeRefresh.refreshes()
|
||||
.onEach {
|
||||
updateLibrary()
|
||||
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
viewScope.launch {
|
||||
presenter.updates.collectLatest { updatesItems ->
|
||||
destroyActionModeIfNeeded()
|
||||
if (adapter == null) {
|
||||
adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, updatesItems)
|
||||
binding.recycler.adapter = adapter
|
||||
adapter!!.fastScroller = binding.fastScroller
|
||||
} else {
|
||||
adapter?.updateDataSet(updatesItems)
|
||||
}
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.fastScroller.isVisible = true
|
||||
binding.recycler.onAnimationsFinished {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
destroyActionModeIfNeeded()
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.updates, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_update_library -> updateLibrary()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun updateLibrary() {
|
||||
@@ -154,262 +70,67 @@ class UpdatesController :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected chapters
|
||||
* @return list of selected chapters
|
||||
*/
|
||||
private fun getSelectedChapters(): List<UpdatesItem> {
|
||||
val adapter = adapter ?: return emptyList()
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? UpdatesItem }
|
||||
// Let compose view handle this
|
||||
override fun handleBack(): Boolean {
|
||||
(activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
|
||||
// Get item from position
|
||||
val item = adapter.getItem(position) as? UpdatesItem ?: return false
|
||||
return if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
true
|
||||
} else {
|
||||
openChapter(item)
|
||||
false
|
||||
}
|
||||
private fun onBackClicked() {
|
||||
(activity as? MainActivity)?.moveToStartScreen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is long clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity
|
||||
if (actionMode == null && activity is MainActivity) {
|
||||
actionMode = activity.startActionModeAndToolbar(this)
|
||||
activity.showBottomNav(false)
|
||||
}
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to toggle selection
|
||||
* @param position position of selected item
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
adapter.toggleSelection(position)
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open chapter in reader
|
||||
* @param chapter selected chapter
|
||||
*/
|
||||
private fun openChapter(item: UpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.manga.id, item.chapter.id)
|
||||
startActivity(intent)
|
||||
private fun toggleNavBarVisibility(navBarVisibility: NavBarVisibility) {
|
||||
val showNavBar = navBarVisibility.toBoolean()
|
||||
(activity as? MainActivity)?.showBottomNav(showNavBar)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected items
|
||||
* @param chapters list of selected [UpdatesItem]s
|
||||
* @param items list of selected [UpdatesItem]s
|
||||
*/
|
||||
private fun downloadChapters(chapters: List<UpdatesItem>) {
|
||||
presenter.downloadChapters(chapters)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
override fun onUpdateEmptyView(size: Int) {
|
||||
if (size > 0) {
|
||||
binding.emptyView.hide()
|
||||
} else {
|
||||
binding.emptyView.show(R.string.information_no_recent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download status of chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
fun onChapterDownloadUpdate(download: Download) {
|
||||
adapter?.currentItems
|
||||
?.filterIsInstance<UpdatesItem>()
|
||||
?.find { it.chapter.id == download.chapter.id }?.let {
|
||||
adapter?.updateItem(it, it.status)
|
||||
private fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
||||
if (items.isEmpty()) return
|
||||
viewScope.launch {
|
||||
when (action) {
|
||||
ChapterDownloadAction.START -> {
|
||||
presenter.downloadChapters(items)
|
||||
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
||||
DownloadService.start(activity!!)
|
||||
}
|
||||
}
|
||||
ChapterDownloadAction.START_NOW -> {
|
||||
val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
|
||||
presenter.startDownloadingNow(chapterId)
|
||||
}
|
||||
ChapterDownloadAction.CANCEL -> {
|
||||
val chapterId = items.singleOrNull()?.update?.chapterId ?: return@launch
|
||||
presenter.cancelDownload(chapterId)
|
||||
}
|
||||
ChapterDownloadAction.DELETE -> {
|
||||
presenter.deleteChapters(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as read
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
private fun markAsRead(chapters: List<UpdatesItem>) {
|
||||
presenter.markChapterRead(chapters, true)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as unread
|
||||
* @param chapters list of selected [UpdatesItem]
|
||||
*/
|
||||
private fun markAsUnread(chapters: List<UpdatesItem>) {
|
||||
presenter.markChapterRead(chapters, false)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
override fun deleteChapters(chaptersToDelete: List<UpdatesItem>) {
|
||||
presenter.deleteChapters(chaptersToDelete)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
override fun onCoverClick(position: Int) {
|
||||
destroyActionModeIfNeeded()
|
||||
|
||||
val chapterClicked = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
openManga(chapterClicked)
|
||||
}
|
||||
|
||||
private fun openManga(chapter: UpdatesItem) {
|
||||
router.pushController(MangaController(chapter.manga.id!!))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapters are deleted
|
||||
*/
|
||||
fun onChaptersDeleted() {
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when error while deleting
|
||||
* @param error error message
|
||||
*/
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
logcat(LogPriority.ERROR, error)
|
||||
}
|
||||
|
||||
override fun downloadChapter(position: Int) {
|
||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
if (item.status == Download.State.ERROR) {
|
||||
DownloadService.start(activity!!)
|
||||
} else {
|
||||
downloadChapters(listOf(item))
|
||||
}
|
||||
adapter?.updateItem(item)
|
||||
}
|
||||
|
||||
override fun deleteChapter(position: Int) {
|
||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
deleteChapters(listOf(item))
|
||||
adapter?.updateItem(item)
|
||||
}
|
||||
|
||||
override fun startDownloadNow(position: Int) {
|
||||
val item = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
presenter.startDownloadingNow(item.chapter)
|
||||
}
|
||||
|
||||
private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
|
||||
presenter.bookmarkChapters(chapters, bookmarked)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode created.
|
||||
* @param mode the ActionMode object
|
||||
* @param menu menu object of ActionMode
|
||||
*/
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.generic_selection, menu)
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
|
||||
menuInflater.inflate(R.menu.updates_chapter_selection, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = adapter?.selectedItemCount ?: 0
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
} else {
|
||||
mode.title = count.toString()
|
||||
private fun deleteChaptersWithConfirmation(items: List<UpdatesItem>) {
|
||||
if (items.isEmpty()) return
|
||||
viewScope.launch {
|
||||
val result = MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.confirm_delete_chapters)
|
||||
.await(android.R.string.ok, android.R.string.cancel)
|
||||
if (result == AlertDialog.BUTTON_POSITIVE) presenter.deleteChapters(items)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
||||
val chapters = getSelectedChapters()
|
||||
if (chapters.isEmpty()) return
|
||||
toolbar.findToolbarItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
|
||||
toolbar.findToolbarItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
|
||||
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark }
|
||||
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark }
|
||||
toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
|
||||
toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
|
||||
private fun openChapter(item: UpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode item clicked
|
||||
* @param mode the ActionMode object
|
||||
* @param item item from ActionMode.
|
||||
*/
|
||||
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 ->
|
||||
ConfirmDeleteChaptersDialog(this, getSelectedChapters())
|
||||
.showDialog(router)
|
||||
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())
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode destroyed
|
||||
* @param mode the ActionMode object
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
adapter?.mode = SelectableAdapter.Mode.IDLE
|
||||
adapter?.clearSelection()
|
||||
|
||||
(activity as? MainActivity)?.showBottomNav(true)
|
||||
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
private fun selectAll() {
|
||||
val adapter = adapter ?: return
|
||||
adapter.selectAll()
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
private fun selectInverse() {
|
||||
val adapter = adapter ?: return
|
||||
for (i in 0..adapter.itemCount) {
|
||||
adapter.toggleSelection(i)
|
||||
}
|
||||
actionMode?.invalidate()
|
||||
adapter.notifyDataSetChanged()
|
||||
private fun openManga(item: UpdatesItem) {
|
||||
router.pushController(MangaController(item.update.mangaId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
|
||||
|
||||
/**
|
||||
* Holder that contains chapter item
|
||||
* UI related actions should be called from here.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new recent chapter holder.
|
||||
*/
|
||||
class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter) :
|
||||
BaseChapterHolder(view, adapter) {
|
||||
|
||||
private val binding = UpdatesItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.mangaCover.setOnClickListener {
|
||||
adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
binding.download.listener = downloadActionListener
|
||||
}
|
||||
|
||||
fun bind(item: UpdatesItem) {
|
||||
// Set chapter title
|
||||
binding.chapterTitle.text = item.chapter.name
|
||||
|
||||
// Set manga title
|
||||
binding.mangaTitle.text = item.manga.title
|
||||
|
||||
// Check if chapter is read and/or bookmarked and set correct color
|
||||
if (item.chapter.read) {
|
||||
binding.chapterTitle.setTextColor(adapter.readColor)
|
||||
binding.mangaTitle.setTextColor(adapter.readColor)
|
||||
} else {
|
||||
binding.mangaTitle.setTextColor(adapter.unreadColor)
|
||||
binding.chapterTitle.setTextColor(
|
||||
if (item.chapter.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
// Set bookmark status
|
||||
binding.bookmarkIcon.isVisible = item.chapter.bookmark
|
||||
|
||||
// Set chapter status
|
||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||
binding.download.setState(item.status, item.progress)
|
||||
|
||||
// Set cover
|
||||
binding.mangaCover.dispose()
|
||||
binding.mangaCover.load(item.manga)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterItem
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
|
||||
class UpdatesItem(chapter: Chapter, val manga: Manga, header: DateSectionItem) :
|
||||
BaseChapterItem<UpdatesHolder, DateSectionItem>(chapter, header) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.updates_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): UpdatesHolder {
|
||||
return UpdatesHolder(view, adapter as UpdatesAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: UpdatesHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
holder.bind(this)
|
||||
}
|
||||
}
|
||||
@@ -1,233 +1,321 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.data.manga.mangaChapterMapper
|
||||
import androidx.compose.runtime.Immutable
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.presentation.updates.UpdatesUiModel
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.preference.asHotFlow
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TreeMap
|
||||
|
||||
class UpdatesPresenter(
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
private val getUpdates: GetUpdates = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
) : BasePresenter<UpdatesController>() {
|
||||
|
||||
private val relativeTime: Int = preferences.relativeTime().get()
|
||||
private val dateFormat: DateFormat = preferences.dateFormat()
|
||||
private val _state: MutableStateFlow<UpdatesState> = MutableStateFlow(UpdatesState.Loading)
|
||||
val state: StateFlow<UpdatesState> = _state.asStateFlow()
|
||||
|
||||
private val _updates: MutableStateFlow<List<UpdatesItem>> = MutableStateFlow(listOf())
|
||||
val updates: StateFlow<List<UpdatesItem>> = _updates.asStateFlow()
|
||||
/**
|
||||
* Helper function to update the UI state only if it's currently in success state
|
||||
*/
|
||||
private fun updateSuccessState(func: (UpdatesState.Success) -> UpdatesState.Success) {
|
||||
_state.update { if (it is UpdatesState.Success) func(it) else it }
|
||||
}
|
||||
|
||||
private var incognitoMode = false
|
||||
set(value) {
|
||||
updateSuccessState { it.copy(isIncognitoMode = value) }
|
||||
field = value
|
||||
}
|
||||
private var downloadOnlyMode = false
|
||||
set(value) {
|
||||
updateSuccessState { it.copy(isDownloadedOnlyMode = value) }
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
*/
|
||||
private var observeDownloadsStatusJob: Job? = null
|
||||
private var observeDownloadsPageJob: Job? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
presenterScope.launchIO {
|
||||
subscribeToUpdates()
|
||||
// Set date limit for recent chapters
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.MONTH, -3)
|
||||
}
|
||||
|
||||
getUpdates.subscribe(calendar)
|
||||
.catch { exception ->
|
||||
_state.value = UpdatesState.Error(exception)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
val uiModels = updates.toUpdateUiModels()
|
||||
_state.update { currentState ->
|
||||
when (currentState) {
|
||||
is UpdatesState.Success -> currentState.copy(uiModels)
|
||||
is UpdatesState.Loading, is UpdatesState.Error ->
|
||||
UpdatesState.Success(
|
||||
uiModels = uiModels,
|
||||
isIncognitoMode = incognitoMode,
|
||||
isDownloadedOnlyMode = downloadOnlyMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
observeDownloads()
|
||||
}
|
||||
}
|
||||
|
||||
preferences.incognitoMode()
|
||||
.asHotFlow { incognito ->
|
||||
incognitoMode = incognito
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
|
||||
preferences.downloadedOnly()
|
||||
.asHotFlow { downloadedOnly ->
|
||||
downloadOnlyMode = downloadedOnly
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateUiModels(): List<UpdatesUiModel> {
|
||||
return this.map { update ->
|
||||
val activeDownload = downloadManager.queue.find { update.chapterId == it.chapter.id }
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
update.chapterName,
|
||||
update.scanlator,
|
||||
update.mangaTitle,
|
||||
update.sourceId,
|
||||
)
|
||||
val downloadState = when {
|
||||
activeDownload != null -> activeDownload.status
|
||||
downloaded -> Download.State.DOWNLOADED
|
||||
else -> Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
val item = UpdatesItem(
|
||||
update = update,
|
||||
downloadStateProvider = { downloadState },
|
||||
downloadProgressProvider = { activeDownload?.progress ?: 0 },
|
||||
)
|
||||
UpdatesUiModel.Item(item)
|
||||
}
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L ->
|
||||
UpdatesUiModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun observeDownloads() {
|
||||
observeDownloadsStatusJob?.cancel()
|
||||
observeDownloadsStatusJob = presenterScope.launchIO {
|
||||
downloadManager.queue.getStatusAsFlow()
|
||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||
.collectLatest {
|
||||
withUIContext {
|
||||
onDownloadStatusChange(it)
|
||||
view?.onChapterDownloadUpdate(it)
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeDownloadsPageJob?.cancel()
|
||||
observeDownloadsPageJob = presenterScope.launchIO {
|
||||
downloadManager.queue.getProgressAsFlow()
|
||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||
.collectLatest {
|
||||
withUIContext {
|
||||
view?.onChapterDownloadUpdate(it)
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observable containing recent chapters and date
|
||||
*/
|
||||
private suspend fun subscribeToUpdates() {
|
||||
// Set date limit for recent chapters
|
||||
val cal = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.MONTH, -3)
|
||||
}
|
||||
|
||||
handler
|
||||
.subscribeToList {
|
||||
mangasQueries.getRecentlyUpdated(after = cal.timeInMillis, mangaChapterMapper)
|
||||
}
|
||||
.map { mangaChapter ->
|
||||
val map = TreeMap<Date, MutableList<Pair<Manga, Chapter>>> { d1, d2 -> d2.compareTo(d1) }
|
||||
val byDate = mangaChapter.groupByTo(map) { it.second.dateFetch.toDateKey() }
|
||||
byDate.flatMap { entry ->
|
||||
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
|
||||
entry.value
|
||||
.sortedWith(compareBy({ it.second.dateFetch }, { it.second.chapterNumber })).asReversed()
|
||||
.map { UpdatesItem(it.second, it.first, dateItem) }
|
||||
}
|
||||
}
|
||||
.collectLatest { list ->
|
||||
list.forEach { item ->
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == item.chapter.id }
|
||||
|
||||
// If there's an active download, assign it, otherwise ask the manager if
|
||||
// the chapter is downloaded and assign it to the status.
|
||||
if (download != null) {
|
||||
item.download = download
|
||||
}
|
||||
}
|
||||
setDownloadedChapters(list)
|
||||
|
||||
_updates.value = list
|
||||
|
||||
// Set unread chapter count for bottom bar badge
|
||||
preferences.unreadUpdatesCount().set(list.count { !it.chapter.read })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and assigns the list of downloaded chapters.
|
||||
*
|
||||
* @param items the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(items: List<UpdatesItem>) {
|
||||
for (item in items) {
|
||||
val manga = item.manga
|
||||
val chapter = item.chapter
|
||||
|
||||
if (downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)) {
|
||||
item.status = Download.State.DOWNLOADED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status of chapters.
|
||||
*
|
||||
* @param download download object containing progress.
|
||||
*/
|
||||
private fun onDownloadStatusChange(download: Download) {
|
||||
// Assign the download to the model object.
|
||||
if (download.status == Download.State.QUEUE) {
|
||||
val chapters = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance<UpdatesItem>()
|
||||
val chapter = chapters.find { it.chapter.id == download.chapter.id }
|
||||
if (chapter != null && chapter.download == null) {
|
||||
chapter.download = download
|
||||
private fun updateDownloadState(download: Download) {
|
||||
updateSuccessState { successState ->
|
||||
val modifiedIndex = successState.uiModels.indexOfFirst {
|
||||
it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id
|
||||
}
|
||||
if (modifiedIndex < 0) return@updateSuccessState successState
|
||||
|
||||
val newUiModels = successState.uiModels.toMutableList().apply {
|
||||
var uiModel = removeAt(modifiedIndex)
|
||||
if (uiModel is UpdatesUiModel.Item) {
|
||||
val item = uiModel.item.copy(
|
||||
downloadStateProvider = { download.status },
|
||||
downloadProgressProvider = { download.progress },
|
||||
)
|
||||
uiModel = UpdatesUiModel.Item(item)
|
||||
}
|
||||
add(modifiedIndex, uiModel)
|
||||
}
|
||||
successState.copy(uiModels = newUiModels)
|
||||
}
|
||||
}
|
||||
|
||||
fun startDownloadingNow(chapter: Chapter) {
|
||||
downloadManager.startDownloadNow(chapter.id)
|
||||
fun startDownloadingNow(chapterId: Long) {
|
||||
downloadManager.startDownloadNow(chapterId)
|
||||
}
|
||||
|
||||
fun cancelDownload(chapterId: Long) {
|
||||
val activeDownload = downloadManager.queue.find { chapterId == it.chapter.id } ?: return
|
||||
downloadManager.deletePendingDownload(activeDownload)
|
||||
updateDownloadState(activeDownload.apply { status = Download.State.NOT_DOWNLOADED })
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapter as read
|
||||
*
|
||||
* @param items list of selected chapters
|
||||
* @param read read status
|
||||
* Mark the selected updates list as read/unread.
|
||||
* @param updates the list of selected updates.
|
||||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markChapterRead(items: List<UpdatesItem>, read: Boolean) {
|
||||
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
setReadStatus.await(
|
||||
read = read,
|
||||
values = items
|
||||
.map { it.chapter }
|
||||
values = updates
|
||||
.mapNotNull { getChapter.await(it.update.chapterId) }
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
*
|
||||
* @param chapters list of chapters
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param updates the list of chapters to bookmark.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<UpdatesItem>) {
|
||||
launchIO {
|
||||
try {
|
||||
deleteChaptersInternal(chapters)
|
||||
withUIContext { view?.onChaptersDeleted() }
|
||||
} catch (e: Throwable) {
|
||||
withUIContext { view?.onChaptersDeletedError(e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapters as bookmarked
|
||||
* @param items list of selected chapters
|
||||
* @param bookmarked bookmark status
|
||||
*/
|
||||
fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
|
||||
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
val toUpdate = items.map {
|
||||
ChapterUpdate(
|
||||
bookmark = bookmarked,
|
||||
id = it.chapter.id,
|
||||
)
|
||||
}
|
||||
updateChapter.awaitAll(toUpdate)
|
||||
updates
|
||||
.filterNot { it.update.bookmark == bookmark }
|
||||
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
||||
.let { updateChapter.awaitAll(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected chapters
|
||||
* @param items list of recent chapters seleted.
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param updatesItem the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(items: List<UpdatesItem>) {
|
||||
items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter.toDbChapter())) }
|
||||
fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||
launchIO {
|
||||
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||
for (updates in groupedUpdates) {
|
||||
val mangaId = updates.first().update.mangaId
|
||||
val manga = getManga.await(mangaId) ?: continue
|
||||
// Don't download if source isn't available
|
||||
sourceManager.get(manga.source) ?: continue
|
||||
val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
*
|
||||
* @param items chapters selected
|
||||
* @param updatesItem list of chapters
|
||||
*/
|
||||
private fun deleteChaptersInternal(chapterItems: List<UpdatesItem>) {
|
||||
val itemsByManga = chapterItems.groupBy { it.manga.id }
|
||||
for ((_, items) in itemsByManga) {
|
||||
val manga = items.first().manga
|
||||
val source = sourceManager.get(manga.source) ?: continue
|
||||
val chapters = items.map { it.chapter.toDbChapter() }
|
||||
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
||||
launchIO {
|
||||
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||
val deletedIds = groupedUpdates.flatMap { updates ->
|
||||
val mangaId = updates.first().update.mangaId
|
||||
val manga = getManga.await(mangaId) ?: return@flatMap emptyList()
|
||||
val source = sourceManager.get(manga.source) ?: return@flatMap emptyList()
|
||||
val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() }
|
||||
downloadManager.deleteChapters(chapters, manga, source).mapNotNull { it.id }
|
||||
}
|
||||
updateSuccessState { successState ->
|
||||
val deletedUpdates = successState.uiModels.filter {
|
||||
it is UpdatesUiModel.Item && deletedIds.contains(it.item.update.chapterId)
|
||||
}
|
||||
if (deletedUpdates.isEmpty()) return@updateSuccessState successState
|
||||
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
items.forEach {
|
||||
it.status = Download.State.NOT_DOWNLOADED
|
||||
it.download = null
|
||||
// TODO: Don't do this fake status update
|
||||
val newUiModels = successState.uiModels.toMutableList().apply {
|
||||
deletedUpdates.forEach { deletedUpdate ->
|
||||
val modifiedIndex = indexOf(deletedUpdate)
|
||||
var uiModel = removeAt(modifiedIndex)
|
||||
if (uiModel is UpdatesUiModel.Item) {
|
||||
val item = uiModel.item.copy(
|
||||
downloadStateProvider = { Download.State.NOT_DOWNLOADED },
|
||||
downloadProgressProvider = { 0 },
|
||||
)
|
||||
uiModel = UpdatesUiModel.Item(item)
|
||||
}
|
||||
add(modifiedIndex, uiModel)
|
||||
}
|
||||
}
|
||||
successState.copy(uiModels = newUiModels)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UpdatesState {
|
||||
object Loading : UpdatesState()
|
||||
data class Error(val error: Throwable) : UpdatesState()
|
||||
data class Success(
|
||||
val uiModels: List<UpdatesUiModel>,
|
||||
val isIncognitoMode: Boolean = false,
|
||||
val isDownloadedOnlyMode: Boolean = false,
|
||||
val showSwipeRefreshIndicator: Boolean = false,
|
||||
) : UpdatesState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class UpdatesItem(
|
||||
val update: UpdatesWithRelations,
|
||||
val downloadStateProvider: () -> Download.State,
|
||||
val downloadProgressProvider: () -> Int,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user