package eu.kanade.presentation.updates import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FlipToBack import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton 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 import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.ChapterDownloadAction import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.LazyColumn import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.MangaBottomActionMenu import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.SwipeRefresh import eu.kanade.presentation.components.VerticalFastScroller 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.recent.updates.UpdatesItem import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Dialog import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Event import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.util.Date @Composable fun UpdateScreen( presenter: UpdatesPresenter, onClickCover: (UpdatesItem) -> Unit, onBackClicked: () -> Unit, ) { val internalOnBackPressed = { if (presenter.selectionMode) { presenter.toggleAllSelection(false) } else { onBackClicked() } } BackHandler(onBack = internalOnBackPressed) 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, onUpdateLibrary = { onUpdateLibrary() }, actionModeCounter = presenter.selected.size, onSelectAll = { presenter.toggleAllSelection(true) }, onInvertSelection = { presenter.invertSelection() }, onCancelActionMode = { presenter.toggleAllSelection(false) }, scrollBehavior = scrollBehavior, ) }, bottomBar = { UpdatesBottomBar( selected = presenter.selected, onDownloadChapter = presenter::downloadChapters, onMultiBookmarkClicked = presenter::bookmarkUpdates, onMultiMarkAsReadClicked = presenter::markUpdatesRead, onMultiDeleteClicked = { presenter.dialog = Dialog.DeleteConfirmation(it) }, ) }, ) { contentPadding -> val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding) when { presenter.isLoading -> LoadingScreen() presenter.uiModels.isEmpty() -> EmptyScreen( textResource = R.string.information_no_recent, modifier = Modifier.padding(contentPaddingWithNavBar), ) 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 updatesListState = rememberLazyListState() 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(1000) isRefreshing = false } }, enabled = presenter.selectionMode.not(), indicatorPadding = contentPadding, ) { VerticalFastScroller( listState = updatesListState, topContentPadding = contentPadding.calculateTopPadding(), endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), state = updatesListState, 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) } } } } @Composable private fun UpdatesAppBar( modifier: Modifier = Modifier, incognitoMode: Boolean, downloadedOnlyMode: Boolean, onUpdateLibrary: () -> Unit, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, onCancelActionMode: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, ) { AppBar( modifier = modifier, title = stringResource(R.string.label_recent_updates), actions = { IconButton(onClick = onUpdateLibrary) { Icon( imageVector = Icons.Default.Refresh, contentDescription = stringResource(R.string.action_update_library), ) } }, actionModeCounter = actionModeCounter, onCancelActionMode = onCancelActionMode, actionModeActions = { IconButton(onClick = onSelectAll) { Icon( imageVector = Icons.Default.SelectAll, contentDescription = stringResource(R.string.action_select_all), ) } IconButton(onClick = onInvertSelection) { Icon( imageVector = Icons.Default.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse), ) } }, downloadedOnlyMode = downloadedOnlyMode, incognitoMode = incognitoMode, scrollBehavior = scrollBehavior, ) } @Composable private fun UpdatesBottomBar( selected: List, onDownloadChapter: (List, ChapterDownloadAction) -> Unit, onMultiBookmarkClicked: (List, bookmark: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, read: Boolean) -> Unit, onMultiDeleteClicked: (List) -> Unit, ) { MangaBottomActionMenu( visible = selected.isNotEmpty(), modifier = Modifier.fillMaxWidth(), onBookmarkClicked = { onMultiBookmarkClicked.invoke(selected, true) }.takeIf { selected.any { !it.update.bookmark } }, onRemoveBookmarkClicked = { onMultiBookmarkClicked.invoke(selected, false) }.takeIf { selected.all { it.update.bookmark } }, onMarkAsReadClicked = { onMultiMarkAsReadClicked(selected, true) }.takeIf { selected.any { !it.update.read } }, onMarkAsUnreadClicked = { onMultiMarkAsReadClicked(selected, false) }.takeIf { selected.any { it.update.read } }, onDownloadClicked = { onDownloadChapter(selected, ChapterDownloadAction.START) }.takeIf { selected.any { it.downloadStateProvider() != Download.State.DOWNLOADED } }, onDeleteClicked = { onMultiDeleteClicked(selected) }.takeIf { selected.any { it.downloadStateProvider() == Download.State.DOWNLOADED } }, ) } sealed class UpdatesUiModel { data class Header(val date: Date) : UpdatesUiModel() data class Item(val item: UpdatesItem) : UpdatesUiModel() }