Use Voyager on Updates tab (#8603)
* Use Voyager on Updates tab * Fix back press * Fix selection
This commit is contained in:
parent
7d34ff214c
commit
acc2312384
9 changed files with 372 additions and 349 deletions
|
@ -0,0 +1,27 @@
|
||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import eu.kanade.presentation.util.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListGroupHeader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,14 +1,9 @@
|
||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -21,9 +16,8 @@ fun RelativeDateHeader(
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Text(
|
ListGroupHeader(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
|
||||||
text = remember {
|
text = remember {
|
||||||
date.toRelativeString(
|
date.toRelativeString(
|
||||||
context,
|
context,
|
||||||
|
@ -31,9 +25,5 @@ fun RelativeDateHeader(
|
||||||
dateFormat,
|
dateFormat,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.presentation.updates
|
package eu.kanade.presentation.updates
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
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.material.icons.outlined.SelectAll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
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.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -33,152 +34,103 @@ import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.components.SwipeRefresh
|
import eu.kanade.presentation.components.SwipeRefresh
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
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.UpdatesItem
|
||||||
import eu.kanade.tachiyomi.ui.updates.UpdatesPresenter
|
import eu.kanade.tachiyomi.ui.updates.UpdatesState
|
||||||
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.widget.TachiyomiBottomNavigationView
|
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Date
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateScreen(
|
fun UpdateScreen(
|
||||||
presenter: UpdatesPresenter,
|
state: UpdatesState,
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
incognitoMode: Boolean,
|
||||||
|
downloadedOnlyMode: Boolean,
|
||||||
|
lastUpdated: Long,
|
||||||
|
relativeTime: Int,
|
||||||
onClickCover: (UpdatesItem) -> Unit,
|
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 = {
|
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||||
if (presenter.selectionMode) {
|
|
||||||
presenter.toggleAllSelection(false)
|
|
||||||
} else {
|
|
||||||
onBackClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackHandler(onBack = internalOnBackPressed)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
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(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
UpdatesAppBar(
|
UpdatesAppBar(
|
||||||
incognitoMode = presenter.isIncognitoMode,
|
incognitoMode = incognitoMode,
|
||||||
downloadedOnlyMode = presenter.isDownloadOnly,
|
downloadedOnlyMode = downloadedOnlyMode,
|
||||||
onUpdateLibrary = { onUpdateLibrary() },
|
onUpdateLibrary = { onUpdateLibrary() },
|
||||||
actionModeCounter = presenter.selected.size,
|
actionModeCounter = state.selected.size,
|
||||||
onSelectAll = { presenter.toggleAllSelection(true) },
|
onSelectAll = { onSelectAll(true) },
|
||||||
onInvertSelection = { presenter.invertSelection() },
|
onInvertSelection = { onInvertSelection() },
|
||||||
onCancelActionMode = { presenter.toggleAllSelection(false) },
|
onCancelActionMode = { onSelectAll(false) },
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
UpdatesBottomBar(
|
UpdatesBottomBar(
|
||||||
selected = presenter.selected,
|
selected = state.selected,
|
||||||
onDownloadChapter = presenter::downloadChapters,
|
onDownloadChapter = onDownloadChapter,
|
||||||
onMultiBookmarkClicked = presenter::bookmarkUpdates,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = presenter::markUpdatesRead,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMultiDeleteClicked = {
|
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||||
presenter.dialog = Dialog.DeleteConfirmation(it)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
|
contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
|
|
||||||
when {
|
when {
|
||||||
presenter.isLoading -> LoadingScreen()
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
presenter.uiModels.isEmpty() -> EmptyScreen(
|
state.items.isEmpty() -> EmptyScreen(
|
||||||
textResource = R.string.information_no_recent,
|
textResource = R.string.information_no_recent,
|
||||||
modifier = Modifier.padding(contentPaddingWithNavBar),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
UpdateScreenContent(
|
val scope = rememberCoroutineScope()
|
||||||
presenter = presenter,
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
contentPadding = contentPaddingWithNavBar,
|
|
||||||
onUpdateLibrary = onUpdateLibrary,
|
|
||||||
onClickCover = onClickCover,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
SwipeRefresh(
|
||||||
private fun UpdateScreenContent(
|
refreshing = isRefreshing,
|
||||||
presenter: UpdatesPresenter,
|
onRefresh = {
|
||||||
contentPadding: PaddingValues,
|
val started = onUpdateLibrary()
|
||||||
onUpdateLibrary: () -> Boolean,
|
if (!started) return@SwipeRefresh
|
||||||
onClickCover: (UpdatesItem) -> Unit,
|
scope.launch {
|
||||||
) {
|
// Fake refresh status but hide it after a second as it's a long running task
|
||||||
val context = LocalContext.current
|
isRefreshing = true
|
||||||
val scope = rememberCoroutineScope()
|
delay(1.seconds)
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !state.selectionMode,
|
||||||
|
indicatorPadding = contentPadding,
|
||||||
|
) {
|
||||||
|
FastScrollLazyColumn(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
) {
|
||||||
|
if (lastUpdated > 0L) {
|
||||||
|
updatesLastUpdatedItem(lastUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
SwipeRefresh(
|
updatesUiItems(
|
||||||
refreshing = isRefreshing,
|
uiModels = state.getUiModel(context, relativeTime),
|
||||||
onRefresh = {
|
selectionMode = state.selectionMode,
|
||||||
val started = onUpdateLibrary()
|
onUpdateSelected = onUpdateSelected,
|
||||||
if (!started) return@SwipeRefresh
|
onClickCover = onClickCover,
|
||||||
scope.launch {
|
onClickUpdate = onOpenChapter,
|
||||||
// Fake refresh status but hide it after a second as it's a long running task
|
onDownloadChapter = onDownloadChapter,
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,6 +217,6 @@ private fun UpdatesBottomBar(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class UpdatesUiModel {
|
sealed class UpdatesUiModel {
|
||||||
data class Header(val date: Date) : UpdatesUiModel()
|
data class Header(val date: String) : UpdatesUiModel()
|
||||||
data class Item(val item: UpdatesItem) : 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.Icons
|
||||||
import androidx.compose.material.icons.filled.Bookmark
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.domain.updates.model.UpdatesWithRelations
|
||||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
import eu.kanade.presentation.components.ChapterDownloadIndicator
|
||||||
|
import eu.kanade.presentation.components.ListGroupHeader
|
||||||
import eu.kanade.presentation.components.MangaCover
|
import eu.kanade.presentation.components.MangaCover
|
||||||
import eu.kanade.presentation.components.RelativeDateHeader
|
|
||||||
import eu.kanade.presentation.util.ReadItemAlpha
|
import eu.kanade.presentation.util.ReadItemAlpha
|
||||||
import eu.kanade.presentation.util.padding
|
import eu.kanade.presentation.util.padding
|
||||||
import eu.kanade.presentation.util.selectedBackground
|
import eu.kanade.presentation.util.selectedBackground
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
@ -73,9 +71,7 @@ fun LazyListScope.updatesLastUpdatedItem(
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.updates_last_update_info, time)
|
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,
|
onClickCover: (UpdatesItem) -> Unit,
|
||||||
onClickUpdate: (UpdatesItem) -> Unit,
|
onClickUpdate: (UpdatesItem) -> Unit,
|
||||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||||
relativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = uiModels,
|
items = uiModels,
|
||||||
|
@ -108,11 +102,9 @@ fun LazyListScope.updatesUiItems(
|
||||||
) { item ->
|
) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is UpdatesUiModel.Header -> {
|
is UpdatesUiModel.Header -> {
|
||||||
RelativeDateHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
text = item.date,
|
||||||
relativeTime = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is UpdatesUiModel.Item -> {
|
is UpdatesUiModel.Item -> {
|
||||||
|
@ -130,11 +122,10 @@ fun LazyListScope.updatesUiItems(
|
||||||
else -> onClickUpdate(updatesItem)
|
else -> onClickUpdate(updatesItem)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickCover = { if (selectionMode.not()) onClickCover(updatesItem) },
|
onClickCover = { onClickCover(updatesItem) }.takeIf { !selectionMode },
|
||||||
onDownloadChapter = {
|
onDownloadChapter = { action: ChapterDownloadAction ->
|
||||||
if (selectionMode.not()) onDownloadChapter(listOf(updatesItem), it)
|
onDownloadChapter(listOf(updatesItem), action)
|
||||||
},
|
}.takeIf { !selectionMode },
|
||||||
downloadIndicatorEnabled = selectionMode.not(),
|
|
||||||
downloadStateProvider = updatesItem.downloadStateProvider,
|
downloadStateProvider = updatesItem.downloadStateProvider,
|
||||||
downloadProgressProvider = updatesItem.downloadProgressProvider,
|
downloadProgressProvider = updatesItem.downloadProgressProvider,
|
||||||
)
|
)
|
||||||
|
@ -150,10 +141,9 @@ fun UpdatesUiItem(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onClickCover: () -> Unit,
|
onClickCover: (() -> Unit)?,
|
||||||
onDownloadChapter: (ChapterDownloadAction) -> Unit,
|
onDownloadChapter: ((ChapterDownloadAction) -> Unit)?,
|
||||||
// Download Indicator
|
// Download Indicator
|
||||||
downloadIndicatorEnabled: Boolean,
|
|
||||||
downloadStateProvider: () -> Download.State,
|
downloadStateProvider: () -> Download.State,
|
||||||
downloadProgressProvider: () -> Int,
|
downloadProgressProvider: () -> Int,
|
||||||
) {
|
) {
|
||||||
|
@ -217,8 +207,8 @@ fun UpdatesUiItem(
|
||||||
Text(
|
Text(
|
||||||
text = update.chapterName,
|
text = update.chapterName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = MaterialTheme.typography.bodySmall
|
color = secondaryTextColor,
|
||||||
.copy(color = secondaryTextColor),
|
style = MaterialTheme.typography.bodySmall,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
onTextLayout = { textHeight = it.size.height },
|
onTextLayout = { textHeight = it.size.height },
|
||||||
modifier = Modifier.alpha(textAlpha),
|
modifier = Modifier.alpha(textAlpha),
|
||||||
|
@ -226,11 +216,11 @@ fun UpdatesUiItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterDownloadIndicator(
|
ChapterDownloadIndicator(
|
||||||
enabled = downloadIndicatorEnabled,
|
enabled = onDownloadChapter != null,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
downloadStateProvider = downloadStateProvider,
|
downloadStateProvider = downloadStateProvider,
|
||||||
downloadProgressProvider = downloadProgressProvider,
|
downloadProgressProvider = downloadProgressProvider,
|
||||||
onClick = onDownloadChapter,
|
onClick = { onDownloadChapter?.invoke(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -485,9 +485,8 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
// Updates screen has custom back handler
|
if (router.handleBack()) {
|
||||||
if (router.getControllerWithTag("${R.id.nav_updates}") != null) {
|
// A Router is consuming back press
|
||||||
router.handleBack()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val backstackSize = router.backstackSize
|
val backstackSize = router.backstackSize
|
||||||
|
@ -495,12 +494,10 @@ class MainActivity : BaseActivity() {
|
||||||
if (backstackSize == 1 && startScreen == null) {
|
if (backstackSize == 1 && startScreen == null) {
|
||||||
// Return to start screen
|
// Return to start screen
|
||||||
moveToStartScreen()
|
moveToStartScreen()
|
||||||
} else if (startScreen != null && router.handleBack()) {
|
|
||||||
// Clear selection for Library screen
|
|
||||||
} else if (shouldHandleExitConfirmation()) {
|
} else if (shouldHandleExitConfirmation()) {
|
||||||
// Exit confirmation (resets after 2 seconds)
|
// Exit confirmation (resets after 2 seconds)
|
||||||
lifecycleScope.launchUI { resetExitConfirmation() }
|
lifecycleScope.launchUI { resetExitConfirmation() }
|
||||||
} else if (backstackSize == 1 || !router.handleBack()) {
|
} else if (backstackSize == 1) {
|
||||||
// Regular back (i.e. closing the app)
|
// Regular back (i.e. closing the app)
|
||||||
if (libraryPreferences.autoClearChapterCache().get()) {
|
if (libraryPreferences.autoClearChapterCache().get()) {
|
||||||
chapterCache.clear()
|
chapterCache.clear()
|
||||||
|
|
|
@ -1,39 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.ui.updates
|
package eu.kanade.tachiyomi.ui.updates
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.presentation.updates.UpdateScreen
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
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
|
|
||||||
|
|
||||||
class UpdatesController :
|
|
||||||
FullComposeController<UpdatesPresenter>(),
|
|
||||||
RootController {
|
|
||||||
|
|
||||||
override fun createPresenter() = UpdatesPresenter()
|
|
||||||
|
|
||||||
|
class UpdatesController : BasicFullComposeController(), RootController {
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
UpdateScreen(
|
Navigator(screen = UpdatesScreen)
|
||||||
presenter = presenter,
|
|
||||||
onClickCover = { item ->
|
|
||||||
router.pushController(MangaController(item.update.mangaId))
|
|
||||||
},
|
|
||||||
onBackClicked = {
|
|
||||||
(activity as? MainActivity)?.moveToStartScreen()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(presenter.selectionMode) {
|
|
||||||
(activity as? MainActivity)?.showBottomNav(presenter.selectionMode.not())
|
|
||||||
}
|
|
||||||
LaunchedEffect(presenter.isLoading) {
|
|
||||||
if (!presenter.isLoading) {
|
|
||||||
(activity as? MainActivity)?.ready = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.updates
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.updates.UpdateScreen
|
||||||
|
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
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.ui.updates.UpdatesScreenModel.Event
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
|
object UpdatesScreen : Screen {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val router = LocalRouter.currentOrThrow
|
||||||
|
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
UpdateScreen(
|
||||||
|
state = state,
|
||||||
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
|
incognitoMode = screenModel.isIncognitoMode,
|
||||||
|
downloadedOnlyMode = screenModel.isDownloadOnly,
|
||||||
|
lastUpdated = screenModel.lastUpdated,
|
||||||
|
relativeTime = screenModel.relativeTime,
|
||||||
|
onClickCover = { item -> router.pushController(MangaController(item.update.mangaId)) },
|
||||||
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
|
onInvertSelection = screenModel::invertSelection,
|
||||||
|
onUpdateLibrary = screenModel::updateLibrary,
|
||||||
|
onDownloadChapter = screenModel::downloadChapters,
|
||||||
|
onMultiBookmarkClicked = screenModel::bookmarkUpdates,
|
||||||
|
onMultiMarkAsReadClicked = screenModel::markUpdatesRead,
|
||||||
|
onMultiDeleteClicked = screenModel::showConfirmDeleteChapters,
|
||||||
|
onUpdateSelected = screenModel::toggleSelection,
|
||||||
|
onOpenChapter = {
|
||||||
|
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val onDismissDialog = { screenModel.setDialog(null) }
|
||||||
|
when (val dialog = state.dialog) {
|
||||||
|
is UpdatesScreenModel.Dialog.DeleteConfirmation -> {
|
||||||
|
UpdatesDeleteConfirmationDialog(
|
||||||
|
onDismissRequest = onDismissDialog,
|
||||||
|
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
screenModel.events.collectLatest { event ->
|
||||||
|
when (event) {
|
||||||
|
Event.InternalError -> screenModel.snackbarHostState.showSnackbar(context.getString(R.string.internal_error))
|
||||||
|
is Event.LibraryUpdateTriggered -> {
|
||||||
|
val msg = if (event.started) {
|
||||||
|
R.string.updating_library
|
||||||
|
} else {
|
||||||
|
R.string.update_already_running
|
||||||
|
}
|
||||||
|
screenModel.snackbarHostState.showSnackbar(context.getString(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.selectionMode) {
|
||||||
|
(context as? MainActivity)?.showBottomNav(!state.selectionMode)
|
||||||
|
}
|
||||||
|
LaunchedEffect(state.isLoading) {
|
||||||
|
if (!state.isLoading) {
|
||||||
|
(context as? MainActivity)?.ready = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,16 @@
|
||||||
package eu.kanade.tachiyomi.ui.updates
|
package eu.kanade.tachiyomi.ui.updates
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.core.prefs.asState
|
||||||
import eu.kanade.core.util.addOrRemove
|
import eu.kanade.core.util.addOrRemove
|
||||||
|
import eu.kanade.core.util.insertSeparators
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||||
|
@ -16,27 +22,27 @@ import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.updates.UpdatesState
|
import eu.kanade.presentation.updates.UpdatesUiModel
|
||||||
import eu.kanade.presentation.updates.UpdatesStateImpl
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||||
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -45,8 +51,7 @@ import java.text.DateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class UpdatesPresenter(
|
class UpdatesScreenModel(
|
||||||
private val state: UpdatesStateImpl = UpdatesState() as UpdatesStateImpl,
|
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val downloadCache: DownloadCache = Injekt.get(),
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
|
@ -55,30 +60,29 @@ class UpdatesPresenter(
|
||||||
private val getUpdates: GetUpdates = Injekt.get(),
|
private val getUpdates: GetUpdates = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
basePreferences: BasePreferences = Injekt.get(),
|
basePreferences: BasePreferences = Injekt.get(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
uiPreferences: UiPreferences = Injekt.get(),
|
||||||
libraryPreferences: LibraryPreferences = Injekt.get(),
|
libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
) : BasePresenter<UpdatesController>(), UpdatesState by state {
|
) : StateScreenModel<UpdatesState>(UpdatesState()) {
|
||||||
|
|
||||||
val isDownloadOnly: Boolean by basePreferences.downloadedOnly().asState()
|
|
||||||
val isIncognitoMode: Boolean by basePreferences.incognitoMode().asState()
|
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState()
|
|
||||||
|
|
||||||
val relativeTime: Int by uiPreferences.relativeTime().asState()
|
|
||||||
val dateFormat: DateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
|
val isDownloadOnly: Boolean by basePreferences.downloadedOnly().asState(coroutineScope)
|
||||||
|
val isIncognitoMode: Boolean by basePreferences.incognitoMode().asState(coroutineScope)
|
||||||
|
|
||||||
|
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
||||||
|
|
||||||
|
val relativeTime: Int by uiPreferences.relativeTime().asState(coroutineScope)
|
||||||
|
val dateFormat: DateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
|
|
||||||
// First and last selected index in list
|
// First and last selected index in list
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
init {
|
||||||
super.onCreate(savedState)
|
coroutineScope.launchIO {
|
||||||
|
|
||||||
presenterScope.launchIO {
|
|
||||||
// Set date limit for recent chapters
|
// Set date limit for recent chapters
|
||||||
val calendar = Calendar.getInstance().apply {
|
val calendar = Calendar.getInstance().apply {
|
||||||
time = Date()
|
time = Date()
|
||||||
|
@ -89,35 +93,24 @@ class UpdatesPresenter(
|
||||||
getUpdates.subscribe(calendar).distinctUntilChanged(),
|
getUpdates.subscribe(calendar).distinctUntilChanged(),
|
||||||
downloadCache.changes,
|
downloadCache.changes,
|
||||||
) { updates, _ -> updates }
|
) { updates, _ -> updates }
|
||||||
.onStart { delay(500) } // Defer to avoid crashing on initial render
|
|
||||||
.catch {
|
.catch {
|
||||||
logcat(LogPriority.ERROR, it)
|
logcat(LogPriority.ERROR, it)
|
||||||
_events.send(Event.InternalError)
|
_events.send(Event.InternalError)
|
||||||
}
|
}
|
||||||
.collectLatest { updates ->
|
.collectLatest { updates ->
|
||||||
state.items = updates.toUpdateItems()
|
mutableState.update {
|
||||||
state.isLoading = false
|
it.copy(
|
||||||
}
|
isLoading = false,
|
||||||
}
|
items = updates.toUpdateItems(),
|
||||||
|
)
|
||||||
presenterScope.launchIO {
|
|
||||||
downloadManager.queue.statusFlow()
|
|
||||||
.catch { logcat(LogPriority.ERROR, it) }
|
|
||||||
.collect {
|
|
||||||
withUIContext {
|
|
||||||
updateDownloadState(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
downloadManager.queue.progressFlow()
|
merge(downloadManager.queue.statusFlow(), downloadManager.queue.progressFlow())
|
||||||
.catch { logcat(LogPriority.ERROR, it) }
|
.catch { logcat(LogPriority.ERROR, it) }
|
||||||
.collect {
|
.collect(this@UpdatesScreenModel::updateDownloadState)
|
||||||
withUIContext {
|
|
||||||
updateDownloadState(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,37 +137,46 @@ class UpdatesPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateLibrary(): Boolean {
|
||||||
|
val started = LibraryUpdateService.start(Injekt.get<Application>())
|
||||||
|
coroutineScope.launch {
|
||||||
|
_events.send(Event.LibraryUpdateTriggered(started))
|
||||||
|
}
|
||||||
|
return started
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status of chapters.
|
* Update status of chapters.
|
||||||
*
|
*
|
||||||
* @param download download object containing progress.
|
* @param download download object containing progress.
|
||||||
*/
|
*/
|
||||||
private fun updateDownloadState(download: Download) {
|
private fun updateDownloadState(download: Download) {
|
||||||
state.items = items.toMutableList().apply {
|
mutableState.update { state ->
|
||||||
val modifiedIndex = indexOfFirst {
|
val newItems = state.items.toMutableList().apply {
|
||||||
it.update.chapterId == download.chapter.id
|
val modifiedIndex = indexOfFirst { it.update.chapterId == download.chapter.id }
|
||||||
}
|
if (modifiedIndex < 0) return@apply
|
||||||
if (modifiedIndex < 0) return@apply
|
|
||||||
|
|
||||||
val item = get(modifiedIndex)
|
val item = get(modifiedIndex)
|
||||||
set(
|
set(
|
||||||
modifiedIndex,
|
modifiedIndex,
|
||||||
item.copy(
|
item.copy(
|
||||||
downloadStateProvider = { download.status },
|
downloadStateProvider = { download.status },
|
||||||
downloadProgressProvider = { download.progress },
|
downloadProgressProvider = { download.progress },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
presenterScope.launch {
|
coroutineScope.launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
ChapterDownloadAction.START -> {
|
ChapterDownloadAction.START -> {
|
||||||
downloadChapters(items)
|
downloadChapters(items)
|
||||||
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
||||||
DownloadService.start(view!!.activity!!)
|
DownloadService.start(Injekt.get<Application>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterDownloadAction.START_NOW -> {
|
ChapterDownloadAction.START_NOW -> {
|
||||||
|
@ -209,7 +211,7 @@ class UpdatesPresenter(
|
||||||
* @param read whether to mark chapters as read or unread.
|
* @param read whether to mark chapters as read or unread.
|
||||||
*/
|
*/
|
||||||
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
setReadStatus.await(
|
setReadStatus.await(
|
||||||
read = read,
|
read = read,
|
||||||
chapters = updates
|
chapters = updates
|
||||||
|
@ -217,6 +219,7 @@ class UpdatesPresenter(
|
||||||
.toTypedArray(),
|
.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
toggleAllSelection(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -224,20 +227,21 @@ class UpdatesPresenter(
|
||||||
* @param updates the list of chapters to bookmark.
|
* @param updates the list of chapters to bookmark.
|
||||||
*/
|
*/
|
||||||
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
updates
|
updates
|
||||||
.filterNot { it.update.bookmark == bookmark }
|
.filterNot { it.update.bookmark == bookmark }
|
||||||
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
||||||
.let { updateChapter.awaitAll(it) }
|
.let { updateChapter.awaitAll(it) }
|
||||||
}
|
}
|
||||||
|
toggleAllSelection(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the given list of chapters with the manager.
|
* Downloads the given list of chapters with the manager.
|
||||||
* @param updatesItem the list of chapters to download.
|
* @param updatesItem the list of chapters to download.
|
||||||
*/
|
*/
|
||||||
fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
private fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||||
presenterScope.launchNonCancellable {
|
coroutineScope.launchNonCancellable {
|
||||||
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||||
for (updates in groupedUpdates) {
|
for (updates in groupedUpdates) {
|
||||||
val mangaId = updates.first().update.mangaId
|
val mangaId = updates.first().update.mangaId
|
||||||
|
@ -256,7 +260,7 @@ class UpdatesPresenter(
|
||||||
* @param updatesItem list of chapters
|
* @param updatesItem list of chapters
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
||||||
presenterScope.launchNonCancellable {
|
coroutineScope.launchNonCancellable {
|
||||||
updatesItem
|
updatesItem
|
||||||
.groupBy { it.update.mangaId }
|
.groupBy { it.update.mangaId }
|
||||||
.entries
|
.entries
|
||||||
|
@ -267,6 +271,11 @@ class UpdatesPresenter(
|
||||||
downloadManager.deleteChapters(chapters, manga, source)
|
downloadManager.deleteChapters(chapters, manga, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toggleAllSelection(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showConfirmDeleteChapters(updatesItem: List<UpdatesItem>) {
|
||||||
|
setDialog(Dialog.DeleteConfirmation(updatesItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSelection(
|
fun toggleSelection(
|
||||||
|
@ -275,85 +284,132 @@ class UpdatesPresenter(
|
||||||
userSelected: Boolean = false,
|
userSelected: Boolean = false,
|
||||||
fromLongPress: Boolean = false,
|
fromLongPress: Boolean = false,
|
||||||
) {
|
) {
|
||||||
state.items = items.toMutableList().apply {
|
mutableState.update { state ->
|
||||||
val selectedIndex = indexOfFirst { it.update.chapterId == item.update.chapterId }
|
val newItems = state.items.toMutableList().apply {
|
||||||
if (selectedIndex < 0) return@apply
|
val selectedIndex = indexOfFirst { it.update.chapterId == item.update.chapterId }
|
||||||
|
if (selectedIndex < 0) return@apply
|
||||||
|
|
||||||
val selectedItem = get(selectedIndex)
|
val selectedItem = get(selectedIndex)
|
||||||
if (selectedItem.selected == selected) return@apply
|
if (selectedItem.selected == selected) return@apply
|
||||||
|
|
||||||
val firstSelection = none { it.selected }
|
val firstSelection = none { it.selected }
|
||||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||||
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
||||||
|
|
||||||
if (selected && userSelected && fromLongPress) {
|
if (selected && userSelected && fromLongPress) {
|
||||||
if (firstSelection) {
|
if (firstSelection) {
|
||||||
selectedPositions[0] = selectedIndex
|
|
||||||
selectedPositions[1] = selectedIndex
|
|
||||||
} else {
|
|
||||||
// Try to select the items in-between when possible
|
|
||||||
val range: IntRange
|
|
||||||
if (selectedIndex < selectedPositions[0]) {
|
|
||||||
range = selectedIndex + 1 until selectedPositions[0]
|
|
||||||
selectedPositions[0] = selectedIndex
|
selectedPositions[0] = selectedIndex
|
||||||
} else if (selectedIndex > selectedPositions[1]) {
|
|
||||||
range = (selectedPositions[1] + 1) until selectedIndex
|
|
||||||
selectedPositions[1] = selectedIndex
|
selectedPositions[1] = selectedIndex
|
||||||
} else {
|
} else {
|
||||||
// Just select itself
|
// Try to select the items in-between when possible
|
||||||
range = IntRange.EMPTY
|
val range: IntRange
|
||||||
}
|
if (selectedIndex < selectedPositions[0]) {
|
||||||
|
range = selectedIndex + 1 until selectedPositions[0]
|
||||||
|
selectedPositions[0] = selectedIndex
|
||||||
|
} else if (selectedIndex > selectedPositions[1]) {
|
||||||
|
range = (selectedPositions[1] + 1) until selectedIndex
|
||||||
|
selectedPositions[1] = selectedIndex
|
||||||
|
} else {
|
||||||
|
// Just select itself
|
||||||
|
range = IntRange.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
range.forEach {
|
range.forEach {
|
||||||
val inbetweenItem = get(it)
|
val inbetweenItem = get(it)
|
||||||
if (!inbetweenItem.selected) {
|
if (!inbetweenItem.selected) {
|
||||||
selectedChapterIds.add(inbetweenItem.update.chapterId)
|
selectedChapterIds.add(inbetweenItem.update.chapterId)
|
||||||
set(it, inbetweenItem.copy(selected = true))
|
set(it, inbetweenItem.copy(selected = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (userSelected && !fromLongPress) {
|
||||||
|
if (!selected) {
|
||||||
|
if (selectedIndex == selectedPositions[0]) {
|
||||||
|
selectedPositions[0] = indexOfFirst { it.selected }
|
||||||
|
} else if (selectedIndex == selectedPositions[1]) {
|
||||||
|
selectedPositions[1] = indexOfLast { it.selected }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedIndex < selectedPositions[0]) {
|
||||||
|
selectedPositions[0] = selectedIndex
|
||||||
|
} else if (selectedIndex > selectedPositions[1]) {
|
||||||
|
selectedPositions[1] = selectedIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (userSelected && !fromLongPress) {
|
|
||||||
if (!selected) {
|
|
||||||
if (selectedIndex == selectedPositions[0]) {
|
|
||||||
selectedPositions[0] = indexOfFirst { it.selected }
|
|
||||||
} else if (selectedIndex == selectedPositions[1]) {
|
|
||||||
selectedPositions[1] = indexOfLast { it.selected }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedIndex < selectedPositions[0]) {
|
|
||||||
selectedPositions[0] = selectedIndex
|
|
||||||
} else if (selectedIndex > selectedPositions[1]) {
|
|
||||||
selectedPositions[1] = selectedIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAllSelection(selected: Boolean) {
|
fun toggleAllSelection(selected: Boolean) {
|
||||||
state.items = items.map {
|
mutableState.update { state ->
|
||||||
selectedChapterIds.addOrRemove(it.update.chapterId, selected)
|
val newItems = state.items.map {
|
||||||
it.copy(selected = selected)
|
selectedChapterIds.addOrRemove(it.update.chapterId, selected)
|
||||||
|
it.copy(selected = selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedPositions[0] = -1
|
selectedPositions[0] = -1
|
||||||
selectedPositions[1] = -1
|
selectedPositions[1] = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invertSelection() {
|
fun invertSelection() {
|
||||||
state.items = items.map {
|
mutableState.update { state ->
|
||||||
selectedChapterIds.addOrRemove(it.update.chapterId, !it.selected)
|
val newItems = state.items.map {
|
||||||
it.copy(selected = !it.selected)
|
selectedChapterIds.addOrRemove(it.update.chapterId, !it.selected)
|
||||||
|
it.copy(selected = !it.selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
}
|
}
|
||||||
selectedPositions[0] = -1
|
selectedPositions[0] = -1
|
||||||
selectedPositions[1] = -1
|
selectedPositions[1] = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setDialog(dialog: Dialog?) {
|
||||||
|
mutableState.update { it.copy(dialog = dialog) }
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Dialog {
|
sealed class Dialog {
|
||||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog()
|
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
object InternalError : Event()
|
object InternalError : Event()
|
||||||
|
data class LibraryUpdateTriggered(val started: Boolean) : Event()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class UpdatesState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val items: List<UpdatesItem> = emptyList(),
|
||||||
|
val dialog: UpdatesScreenModel.Dialog? = null,
|
||||||
|
) {
|
||||||
|
val selected = items.filter { it.selected }
|
||||||
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
||||||
|
fun getUiModel(context: Context, relativeTime: Int): List<UpdatesUiModel> {
|
||||||
|
val dateFormat = UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get())
|
||||||
|
return items
|
||||||
|
.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 -> {
|
||||||
|
val text = afterDate.toRelativeString(
|
||||||
|
context = context,
|
||||||
|
range = relativeTime,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
)
|
||||||
|
UpdatesUiModel.Header(text)
|
||||||
|
}
|
||||||
|
// Return null to avoid adding a separator between two items.
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue