mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 03:58:56 +01:00
Use Voyager on Updates tab (#8603)
* Use Voyager on Updates tab * Fix back press * Fix selection
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -10,9 +9,11 @@ import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -33,152 +34,103 @@ import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.SwipeRefresh
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesPresenter
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesPresenter.Dialog
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesPresenter.Event
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesState
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun UpdateScreen(
|
||||
presenter: UpdatesPresenter,
|
||||
state: UpdatesState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
lastUpdated: Long,
|
||||
relativeTime: Int,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
onSelectAll: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
onUpdateLibrary: () -> Boolean,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onOpenChapter: (UpdatesItem) -> Unit,
|
||||
) {
|
||||
val internalOnBackPressed = {
|
||||
if (presenter.selectionMode) {
|
||||
presenter.toggleAllSelection(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||
|
||||
val context = LocalContext.current
|
||||
val onUpdateLibrary = {
|
||||
val started = LibraryUpdateService.start(context)
|
||||
context.toast(if (started) R.string.updating_library else R.string.update_already_running)
|
||||
started
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
UpdatesAppBar(
|
||||
incognitoMode = presenter.isIncognitoMode,
|
||||
downloadedOnlyMode = presenter.isDownloadOnly,
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
onUpdateLibrary = { onUpdateLibrary() },
|
||||
actionModeCounter = presenter.selected.size,
|
||||
onSelectAll = { presenter.toggleAllSelection(true) },
|
||||
onInvertSelection = { presenter.invertSelection() },
|
||||
onCancelActionMode = { presenter.toggleAllSelection(false) },
|
||||
actionModeCounter = state.selected.size,
|
||||
onSelectAll = { onSelectAll(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
onCancelActionMode = { onSelectAll(false) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
UpdatesBottomBar(
|
||||
selected = presenter.selected,
|
||||
onDownloadChapter = presenter::downloadChapters,
|
||||
onMultiBookmarkClicked = presenter::bookmarkUpdates,
|
||||
onMultiMarkAsReadClicked = presenter::markUpdatesRead,
|
||||
onMultiDeleteClicked = {
|
||||
presenter.dialog = Dialog.DeleteConfirmation(it)
|
||||
},
|
||||
selected = state.selected,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
|
||||
) { contentPadding ->
|
||||
val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.uiModels.isEmpty() -> EmptyScreen(
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.items.isEmpty() -> EmptyScreen(
|
||||
textResource = R.string.information_no_recent,
|
||||
modifier = Modifier.padding(contentPaddingWithNavBar),
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
UpdateScreenContent(
|
||||
presenter = presenter,
|
||||
contentPadding = contentPaddingWithNavBar,
|
||||
onUpdateLibrary = onUpdateLibrary,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
private fun UpdateScreenContent(
|
||||
presenter: UpdatesPresenter,
|
||||
contentPadding: PaddingValues,
|
||||
onUpdateLibrary: () -> Boolean,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
SwipeRefresh(
|
||||
refreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
val started = onUpdateLibrary()
|
||||
if (!started) return@SwipeRefresh
|
||||
scope.launch {
|
||||
// Fake refresh status but hide it after a second as it's a long running task
|
||||
isRefreshing = true
|
||||
delay(1.seconds)
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = !state.selectionMode,
|
||||
indicatorPadding = contentPadding,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
if (lastUpdated > 0L) {
|
||||
updatesLastUpdatedItem(lastUpdated)
|
||||
}
|
||||
|
||||
SwipeRefresh(
|
||||
refreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
val started = onUpdateLibrary()
|
||||
if (!started) return@SwipeRefresh
|
||||
scope.launch {
|
||||
// Fake refresh status but hide it after a second as it's a long running task
|
||||
isRefreshing = true
|
||||
delay(1.seconds)
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = presenter.selectionMode.not(),
|
||||
indicatorPadding = contentPadding,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
if (presenter.lastUpdated > 0L) {
|
||||
updatesLastUpdatedItem(presenter.lastUpdated)
|
||||
}
|
||||
|
||||
updatesUiItems(
|
||||
uiModels = presenter.uiModels,
|
||||
selectionMode = presenter.selectionMode,
|
||||
onUpdateSelected = presenter::toggleSelection,
|
||||
onClickCover = onClickCover,
|
||||
onClickUpdate = {
|
||||
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
onDownloadChapter = presenter::downloadChapters,
|
||||
relativeTime = presenter.relativeTime,
|
||||
dateFormat = presenter.dateFormat,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val onDismissDialog = { presenter.dialog = null }
|
||||
when (val dialog = presenter.dialog) {
|
||||
is Dialog.DeleteConfirmation -> {
|
||||
UpdatesDeleteConfirmationDialog(
|
||||
onDismissRequest = onDismissDialog,
|
||||
onConfirm = {
|
||||
presenter.toggleAllSelection(false)
|
||||
presenter.deleteChapters(dialog.toDelete)
|
||||
},
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest { event ->
|
||||
when (event) {
|
||||
Event.InternalError -> context.toast(R.string.internal_error)
|
||||
updatesUiItems(
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
selectionMode = state.selectionMode,
|
||||
onUpdateSelected = onUpdateSelected,
|
||||
onClickCover = onClickCover,
|
||||
onClickUpdate = onOpenChapter,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,6 +217,6 @@ private fun UpdatesBottomBar(
|
||||
}
|
||||
|
||||
sealed class UpdatesUiModel {
|
||||
data class Header(val date: Date) : UpdatesUiModel()
|
||||
data class Header(val date: String) : UpdatesUiModel()
|
||||
data class Item(val item: UpdatesItem) : UpdatesUiModel()
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesPresenter
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
import java.util.Date
|
||||
|
||||
@Stable
|
||||
interface UpdatesState {
|
||||
val isLoading: Boolean
|
||||
val items: List<UpdatesItem>
|
||||
val selected: List<UpdatesItem>
|
||||
val selectionMode: Boolean
|
||||
val uiModels: List<UpdatesUiModel>
|
||||
var dialog: UpdatesPresenter.Dialog?
|
||||
}
|
||||
fun UpdatesState(): UpdatesState = UpdatesStateImpl()
|
||||
class UpdatesStateImpl : UpdatesState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var items: List<UpdatesItem> by mutableStateOf(emptyList())
|
||||
override val selected: List<UpdatesItem> by derivedStateOf {
|
||||
items.filter { it.selected }
|
||||
}
|
||||
override val selectionMode: Boolean by derivedStateOf { selected.isNotEmpty() }
|
||||
override val uiModels: List<UpdatesUiModel> by derivedStateOf {
|
||||
items.toUpdateUiModel()
|
||||
}
|
||||
override var dialog: UpdatesPresenter.Dialog? by mutableStateOf(null)
|
||||
}
|
||||
|
||||
fun List<UpdatesItem>.toUpdateUiModel(): List<UpdatesUiModel> {
|
||||
return this.map {
|
||||
UpdatesUiModel.Item(it)
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,15 +36,14 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.components.ListGroupHeader
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.components.RelativeDateHeader
|
||||
import eu.kanade.presentation.util.ReadItemAlpha
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.presentation.util.selectedBackground
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@@ -73,9 +71,7 @@ fun LazyListScope.updatesLastUpdatedItem(
|
||||
} else {
|
||||
stringResource(R.string.updates_last_update_info, time)
|
||||
},
|
||||
style = LocalTextStyle.current.copy(
|
||||
fontStyle = FontStyle.Italic,
|
||||
),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -88,8 +84,6 @@ fun LazyListScope.updatesUiItems(
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
relativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
) {
|
||||
items(
|
||||
items = uiModels,
|
||||
@@ -108,11 +102,9 @@ fun LazyListScope.updatesUiItems(
|
||||
) { item ->
|
||||
when (item) {
|
||||
is UpdatesUiModel.Header -> {
|
||||
RelativeDateHeader(
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
text = item.date,
|
||||
)
|
||||
}
|
||||
is UpdatesUiModel.Item -> {
|
||||
@@ -130,11 +122,10 @@ fun LazyListScope.updatesUiItems(
|
||||
else -> onClickUpdate(updatesItem)
|
||||
}
|
||||
},
|
||||
onClickCover = { if (selectionMode.not()) onClickCover(updatesItem) },
|
||||
onDownloadChapter = {
|
||||
if (selectionMode.not()) onDownloadChapter(listOf(updatesItem), it)
|
||||
},
|
||||
downloadIndicatorEnabled = selectionMode.not(),
|
||||
onClickCover = { onClickCover(updatesItem) }.takeIf { !selectionMode },
|
||||
onDownloadChapter = { action: ChapterDownloadAction ->
|
||||
onDownloadChapter(listOf(updatesItem), action)
|
||||
}.takeIf { !selectionMode },
|
||||
downloadStateProvider = updatesItem.downloadStateProvider,
|
||||
downloadProgressProvider = updatesItem.downloadProgressProvider,
|
||||
)
|
||||
@@ -150,10 +141,9 @@ fun UpdatesUiItem(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onClickCover: () -> Unit,
|
||||
onDownloadChapter: (ChapterDownloadAction) -> Unit,
|
||||
onClickCover: (() -> Unit)?,
|
||||
onDownloadChapter: ((ChapterDownloadAction) -> Unit)?,
|
||||
// Download Indicator
|
||||
downloadIndicatorEnabled: Boolean,
|
||||
downloadStateProvider: () -> Download.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
) {
|
||||
@@ -217,8 +207,8 @@ fun UpdatesUiItem(
|
||||
Text(
|
||||
text = update.chapterName,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
.copy(color = secondaryTextColor),
|
||||
color = secondaryTextColor,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
modifier = Modifier.alpha(textAlpha),
|
||||
@@ -226,11 +216,11 @@ fun UpdatesUiItem(
|
||||
}
|
||||
}
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
enabled = onDownloadChapter != null,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadChapter,
|
||||
onClick = { onDownloadChapter?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user