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
|
||||
|
||||
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.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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 java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
@ -21,9 +16,8 @@ fun RelativeDateHeader(
|
|||
dateFormat: DateFormat,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
|
||||
ListGroupHeader(
|
||||
modifier = modifier,
|
||||
text = remember {
|
||||
date.toRelativeString(
|
||||
context,
|
||||
|
@ -31,9 +25,5 @@ fun RelativeDateHeader(
|
|||
dateFormat,
|
||||
)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,94 +34,68 @@ 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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) }
|
||||
|
||||
|
@ -136,49 +111,26 @@ private fun UpdateScreenContent(
|
|||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = presenter.selectionMode.not(),
|
||||
enabled = !state.selectionMode,
|
||||
indicatorPadding = contentPadding,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
if (presenter.lastUpdated > 0L) {
|
||||
updatesLastUpdatedItem(presenter.lastUpdated)
|
||||
if (lastUpdated > 0L) {
|
||||
updatesLastUpdatedItem(lastUpdated)
|
||||
}
|
||||
|
||||
updatesUiItems(
|
||||
uiModels = presenter.uiModels,
|
||||
selectionMode = presenter.selectionMode,
|
||||
onUpdateSelected = presenter::toggleSelection,
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
selectionMode = state.selectionMode,
|
||||
onUpdateSelected = onUpdateSelected,
|
||||
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,
|
||||
onClickUpdate = onOpenChapter,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -485,9 +485,8 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Updates screen has custom back handler
|
||||
if (router.getControllerWithTag("${R.id.nav_updates}") != null) {
|
||||
router.handleBack()
|
||||
if (router.handleBack()) {
|
||||
// A Router is consuming back press
|
||||
return
|
||||
}
|
||||
val backstackSize = router.backstackSize
|
||||
|
@ -495,12 +494,10 @@ class MainActivity : BaseActivity() {
|
|||
if (backstackSize == 1 && startScreen == null) {
|
||||
// Return to start screen
|
||||
moveToStartScreen()
|
||||
} else if (startScreen != null && router.handleBack()) {
|
||||
// Clear selection for Library screen
|
||||
} else if (shouldHandleExitConfirmation()) {
|
||||
// Exit confirmation (resets after 2 seconds)
|
||||
lifecycleScope.launchUI { resetExitConfirmation() }
|
||||
} else if (backstackSize == 1 || !router.handleBack()) {
|
||||
} else if (backstackSize == 1) {
|
||||
// Regular back (i.e. closing the app)
|
||||
if (libraryPreferences.autoClearChapterCache().get()) {
|
||||
chapterCache.clear()
|
||||
|
|
|
@ -1,39 +1,13 @@
|
|||
package eu.kanade.tachiyomi.ui.updates
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
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
|
||||
override fun ComposeContent() {
|
||||
UpdateScreen(
|
||||
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
|
||||
}
|
||||
}
|
||||
Navigator(screen = UpdatesScreen)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
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.getValue
|
||||
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.insertSeparators
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
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.model.UpdatesWithRelations
|
||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.updates.UpdatesState
|
||||
import eu.kanade.presentation.updates.UpdatesStateImpl
|
||||
import eu.kanade.presentation.updates.UpdatesUiModel
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
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 kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -45,8 +51,7 @@ import java.text.DateFormat
|
|||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
class UpdatesPresenter(
|
||||
private val state: UpdatesStateImpl = UpdatesState() as UpdatesStateImpl,
|
||||
class UpdatesScreenModel(
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val downloadCache: DownloadCache = Injekt.get(),
|
||||
|
@ -55,30 +60,29 @@ class UpdatesPresenter(
|
|||
private val getUpdates: GetUpdates = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
basePreferences: BasePreferences = Injekt.get(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) : BasePresenter<UpdatesController>(), UpdatesState by state {
|
||||
|
||||
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()))
|
||||
) : StateScreenModel<UpdatesState>(UpdatesState()) {
|
||||
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
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
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
presenterScope.launchIO {
|
||||
init {
|
||||
coroutineScope.launchIO {
|
||||
// Set date limit for recent chapters
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
|
@ -89,35 +93,24 @@ class UpdatesPresenter(
|
|||
getUpdates.subscribe(calendar).distinctUntilChanged(),
|
||||
downloadCache.changes,
|
||||
) { updates, _ -> updates }
|
||||
.onStart { delay(500) } // Defer to avoid crashing on initial render
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
_events.send(Event.InternalError)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
state.items = updates.toUpdateItems()
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
presenterScope.launchIO {
|
||||
downloadManager.queue.statusFlow()
|
||||
.catch { logcat(LogPriority.ERROR, it) }
|
||||
.collect {
|
||||
withUIContext {
|
||||
updateDownloadState(it)
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
items = updates.toUpdateItems(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
presenterScope.launchIO {
|
||||
downloadManager.queue.progressFlow()
|
||||
coroutineScope.launchIO {
|
||||
merge(downloadManager.queue.statusFlow(), downloadManager.queue.progressFlow())
|
||||
.catch { logcat(LogPriority.ERROR, it) }
|
||||
.collect {
|
||||
withUIContext {
|
||||
updateDownloadState(it)
|
||||
}
|
||||
}
|
||||
.collect(this@UpdatesScreenModel::updateDownloadState)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,16 +137,23 @@ 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.
|
||||
*
|
||||
* @param download download object containing progress.
|
||||
*/
|
||||
private fun updateDownloadState(download: Download) {
|
||||
state.items = items.toMutableList().apply {
|
||||
val modifiedIndex = indexOfFirst {
|
||||
it.update.chapterId == download.chapter.id
|
||||
}
|
||||
mutableState.update { state ->
|
||||
val newItems = state.items.toMutableList().apply {
|
||||
val modifiedIndex = indexOfFirst { it.update.chapterId == download.chapter.id }
|
||||
if (modifiedIndex < 0) return@apply
|
||||
|
||||
val item = get(modifiedIndex)
|
||||
|
@ -165,16 +165,18 @@ class UpdatesPresenter(
|
|||
),
|
||||
)
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters(items: List<UpdatesItem>, action: ChapterDownloadAction) {
|
||||
if (items.isEmpty()) return
|
||||
presenterScope.launch {
|
||||
coroutineScope.launch {
|
||||
when (action) {
|
||||
ChapterDownloadAction.START -> {
|
||||
downloadChapters(items)
|
||||
if (items.any { it.downloadStateProvider() == Download.State.ERROR }) {
|
||||
DownloadService.start(view!!.activity!!)
|
||||
DownloadService.start(Injekt.get<Application>())
|
||||
}
|
||||
}
|
||||
ChapterDownloadAction.START_NOW -> {
|
||||
|
@ -209,7 +211,7 @@ class UpdatesPresenter(
|
|||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markUpdatesRead(updates: List<UpdatesItem>, read: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
coroutineScope.launchIO {
|
||||
setReadStatus.await(
|
||||
read = read,
|
||||
chapters = updates
|
||||
|
@ -217,6 +219,7 @@ class UpdatesPresenter(
|
|||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
toggleAllSelection(false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -224,20 +227,21 @@ class UpdatesPresenter(
|
|||
* @param updates the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||
presenterScope.launchIO {
|
||||
coroutineScope.launchIO {
|
||||
updates
|
||||
.filterNot { it.update.bookmark == bookmark }
|
||||
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
||||
.let { updateChapter.awaitAll(it) }
|
||||
}
|
||||
toggleAllSelection(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param updatesItem the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||
presenterScope.launchNonCancellable {
|
||||
private fun downloadChapters(updatesItem: List<UpdatesItem>) {
|
||||
coroutineScope.launchNonCancellable {
|
||||
val groupedUpdates = updatesItem.groupBy { it.update.mangaId }.values
|
||||
for (updates in groupedUpdates) {
|
||||
val mangaId = updates.first().update.mangaId
|
||||
|
@ -256,7 +260,7 @@ class UpdatesPresenter(
|
|||
* @param updatesItem list of chapters
|
||||
*/
|
||||
fun deleteChapters(updatesItem: List<UpdatesItem>) {
|
||||
presenterScope.launchNonCancellable {
|
||||
coroutineScope.launchNonCancellable {
|
||||
updatesItem
|
||||
.groupBy { it.update.mangaId }
|
||||
.entries
|
||||
|
@ -267,6 +271,11 @@ class UpdatesPresenter(
|
|||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
}
|
||||
}
|
||||
toggleAllSelection(false)
|
||||
}
|
||||
|
||||
fun showConfirmDeleteChapters(updatesItem: List<UpdatesItem>) {
|
||||
setDialog(Dialog.DeleteConfirmation(updatesItem))
|
||||
}
|
||||
|
||||
fun toggleSelection(
|
||||
|
@ -275,7 +284,8 @@ class UpdatesPresenter(
|
|||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
state.items = items.toMutableList().apply {
|
||||
mutableState.update { state ->
|
||||
val newItems = state.items.toMutableList().apply {
|
||||
val selectedIndex = indexOfFirst { it.update.chapterId == item.update.chapterId }
|
||||
if (selectedIndex < 0) return@apply
|
||||
|
||||
|
@ -328,32 +338,78 @@ class UpdatesPresenter(
|
|||
}
|
||||
}
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAllSelection(selected: Boolean) {
|
||||
state.items = items.map {
|
||||
mutableState.update { state ->
|
||||
val newItems = state.items.map {
|
||||
selectedChapterIds.addOrRemove(it.update.chapterId, selected)
|
||||
it.copy(selected = selected)
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
state.items = items.map {
|
||||
mutableState.update { state ->
|
||||
val newItems = state.items.map {
|
||||
selectedChapterIds.addOrRemove(it.update.chapterId, !it.selected)
|
||||
it.copy(selected = !it.selected)
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update { it.copy(dialog = dialog) }
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog()
|
||||
}
|
||||
|
||||
sealed class 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