mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-14 04:58:56 +01:00
Use Stable interface for Updates screen + Cleanup (#7627)
* Use Stable interface for Updates screen + Cleanup Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> * Disable swipe refresh in selection mode * Review Changes Co-Authored-By: Andreas <6576096+ghostbear@users.noreply.github.com> * Review Changes 2 Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
This commit is contained in:
@@ -1,28 +1,19 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.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.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.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.await
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -36,39 +27,27 @@ class UpdatesController :
|
||||
|
||||
@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 ->
|
||||
Crossfade(targetState = presenter.isLoading) { isLoading ->
|
||||
if (isLoading) {
|
||||
LoadingScreen()
|
||||
} else {
|
||||
UpdateScreen(
|
||||
state = (state as UpdatesState.Success),
|
||||
onClickCover = this::openManga,
|
||||
onClickUpdate = this::openChapter,
|
||||
onDownloadChapter = this::downloadChapters,
|
||||
onUpdateLibrary = this::updateLibrary,
|
||||
presenter = presenter,
|
||||
onClickCover = { item ->
|
||||
router.pushController(MangaController(item.update.mangaId))
|
||||
},
|
||||
onBackClicked = this::onBackClicked,
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked = { updatesItems, bookmark ->
|
||||
presenter.bookmarkUpdates(updatesItems, bookmark)
|
||||
},
|
||||
onMultiMarkAsReadClicked = { updatesItems, read ->
|
||||
presenter.markUpdatesRead(updatesItems, read)
|
||||
},
|
||||
onMultiDeleteClicked = this::deleteChaptersWithConfirmation,
|
||||
onDownloadChapter = this::downloadChapters,
|
||||
)
|
||||
}
|
||||
LaunchedEffect(state) {
|
||||
if (state !is UpdatesState.Loading) {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLibrary() {
|
||||
activity?.let {
|
||||
if (LibraryUpdateService.start(it)) {
|
||||
it.toast(R.string.updating_library)
|
||||
LaunchedEffect(presenter.selectionMode) {
|
||||
val activity = (activity as? MainActivity) ?: return@LaunchedEffect
|
||||
activity.showBottomNav(presenter.selectionMode.not())
|
||||
}
|
||||
LaunchedEffect(presenter.isLoading) {
|
||||
if (presenter.isLoading.not()) {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,26 +84,7 @@ class UpdatesController :
|
||||
presenter.deleteChapters(items)
|
||||
}
|
||||
}
|
||||
presenter.toggleAllSelection(false)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openChapter(item: UpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun openManga(item: UpdatesItem) {
|
||||
router.pushController(MangaController(item.update.mangaId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
@@ -11,6 +13,8 @@ import eu.kanade.domain.chapter.model.toDbChapter
|
||||
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.UpdatesState
|
||||
import eu.kanade.presentation.updates.UpdatesStateImpl
|
||||
import eu.kanade.presentation.updates.UpdatesUiModel
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
@@ -20,23 +24,22 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
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.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
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
|
||||
|
||||
class UpdatesPresenter(
|
||||
private val state: UpdatesStateImpl = UpdatesState() as UpdatesStateImpl,
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
private val getUpdates: GetUpdates = Injekt.get(),
|
||||
@@ -44,29 +47,22 @@ class UpdatesPresenter(
|
||||
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>() {
|
||||
preferences: PreferencesHelper = Injekt.get(),
|
||||
) : BasePresenter<UpdatesController>(), UpdatesState by state {
|
||||
|
||||
private val _state: MutableStateFlow<UpdatesState> = MutableStateFlow(UpdatesState.Loading)
|
||||
val state: StateFlow<UpdatesState> = _state.asStateFlow()
|
||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
||||
|
||||
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
|
||||
}
|
||||
val relativeTime: Int by preferences.relativeTime().asState()
|
||||
|
||||
val dateFormat: DateFormat by mutableStateOf(preferences.dateFormat())
|
||||
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events: Flow<Event> = _events.receiveAsFlow()
|
||||
|
||||
// First and last selected index in list
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
@@ -85,38 +81,17 @@ class UpdatesPresenter(
|
||||
}
|
||||
|
||||
getUpdates.subscribe(calendar)
|
||||
.catch { exception ->
|
||||
_state.value = UpdatesState.Error(exception)
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
_events.send(Event.InternalError)
|
||||
}
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
state.uiModels = updates.toUpdateUiModels()
|
||||
state.isLoading = false
|
||||
|
||||
observeDownloads()
|
||||
}
|
||||
}
|
||||
|
||||
preferences.incognitoMode()
|
||||
.asHotFlow { incognito ->
|
||||
incognitoMode = incognito
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
|
||||
preferences.downloadedOnly()
|
||||
.asHotFlow { downloadedOnly ->
|
||||
downloadOnlyMode = downloadedOnly
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
}
|
||||
|
||||
private fun List<UpdatesWithRelations>.toUpdateUiModels(): List<UpdatesUiModel> {
|
||||
@@ -182,24 +157,22 @@ class UpdatesPresenter(
|
||||
* @param download download object containing progress.
|
||||
*/
|
||||
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 uiModels = state.uiModels
|
||||
val modifiedIndex = uiModels.indexOfFirst {
|
||||
it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id
|
||||
}
|
||||
if (modifiedIndex < 0) return
|
||||
|
||||
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)
|
||||
state.uiModels = 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)
|
||||
}
|
||||
successState.copy(uiModels = newUiModels)
|
||||
add(modifiedIndex, uiModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,42 +248,131 @@ class UpdatesPresenter(
|
||||
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
|
||||
|
||||
// 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)
|
||||
val uiModels = state.uiModels
|
||||
val deletedUpdates = uiModels.filter {
|
||||
it is UpdatesUiModel.Item && deletedIds.contains(it.item.update.chapterId)
|
||||
}
|
||||
if (deletedUpdates.isEmpty()) return@launchIO
|
||||
|
||||
// TODO: Don't do this fake status update
|
||||
state.uiModels = 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()
|
||||
fun toggleSelection(
|
||||
item: UpdatesItem,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
val uiModels = state.uiModels
|
||||
val modifiedIndex = uiModels.indexOfFirst {
|
||||
it is UpdatesUiModel.Item && it.item.update.chapterId == item.update.chapterId
|
||||
}
|
||||
if (modifiedIndex < 0) return
|
||||
|
||||
val oldItem = (uiModels[modifiedIndex] as? UpdatesUiModel.Item)?.item ?: return
|
||||
if ((oldItem.selected && selected) || (!oldItem.selected && !selected)) return
|
||||
|
||||
state.uiModels = uiModels.toMutableList().apply {
|
||||
val firstSelection = none { it is UpdatesUiModel.Item && it.item.selected }
|
||||
var newItem = (removeAt(modifiedIndex) as? UpdatesUiModel.Item)?.item?.copy(selected = selected) ?: return@apply
|
||||
add(modifiedIndex, UpdatesUiModel.Item(newItem))
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = modifiedIndex
|
||||
selectedPositions[1] = modifiedIndex
|
||||
} else {
|
||||
// Try to select the items in-between when possible
|
||||
val range: IntRange
|
||||
if (modifiedIndex < selectedPositions[0]) {
|
||||
range = modifiedIndex + 1 until selectedPositions[0]
|
||||
selectedPositions[0] = modifiedIndex
|
||||
} else if (modifiedIndex > selectedPositions[1]) {
|
||||
range = (selectedPositions[1] + 1) until modifiedIndex
|
||||
selectedPositions[1] = modifiedIndex
|
||||
} else {
|
||||
// Just select itself
|
||||
range = IntRange.EMPTY
|
||||
}
|
||||
|
||||
range.forEach {
|
||||
var uiModel = removeAt(it)
|
||||
if (uiModel is UpdatesUiModel.Item) {
|
||||
newItem = uiModel.item.copy(selected = true)
|
||||
uiModel = UpdatesUiModel.Item(newItem)
|
||||
}
|
||||
add(it, uiModel)
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
if (!selected) {
|
||||
if (modifiedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it is UpdatesUiModel.Item && it.item.selected }
|
||||
} else if (modifiedIndex == selectedPositions[1]) {
|
||||
selectedPositions[1] = indexOfLast { it is UpdatesUiModel.Item && it.item.selected }
|
||||
}
|
||||
} else {
|
||||
if (modifiedIndex < selectedPositions[0]) {
|
||||
selectedPositions[0] = modifiedIndex
|
||||
} else if (modifiedIndex > selectedPositions[1]) {
|
||||
selectedPositions[1] = modifiedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAllSelection(selected: Boolean) {
|
||||
state.uiModels = state.uiModels.map {
|
||||
when (it) {
|
||||
is UpdatesUiModel.Header -> it
|
||||
is UpdatesUiModel.Item -> {
|
||||
val newItem = it.item.copy(selected = selected)
|
||||
UpdatesUiModel.Item(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
state.uiModels = state.uiModels.map {
|
||||
when (it) {
|
||||
is UpdatesUiModel.Header -> it
|
||||
is UpdatesUiModel.Item -> {
|
||||
val newItem = it.item.let { item -> item.copy(selected = !item.selected) }
|
||||
UpdatesUiModel.Item(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog()
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
object InternalError : Event()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@@ -318,4 +380,5 @@ data class UpdatesItem(
|
||||
val update: UpdatesWithRelations,
|
||||
val downloadStateProvider: () -> Download.State,
|
||||
val downloadProgressProvider: () -> Int,
|
||||
val selected: Boolean = false,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user