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.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 com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState 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.SwipeRefreshIndicator import eu.kanade.presentation.components.VerticalFastScroller import eu.kanade.presentation.util.bottomNavPaddingValues import eu.kanade.presentation.util.plus 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 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 -> when { presenter.isLoading -> LoadingScreen() presenter.uiModels.isEmpty() -> EmptyScreen(textResource = R.string.information_no_recent) else -> { UpdateScreenContent( presenter = presenter, contentPadding = contentPadding, onUpdateLibrary = onUpdateLibrary, onClickCover = onClickCover, ) } } } } @Composable private fun UpdateScreenContent( presenter: UpdatesPresenter, contentPadding: PaddingValues, onUpdateLibrary: () -> Boolean, onClickCover: (UpdatesItem) -> Unit, ) { val context = LocalContext.current val updatesListState = rememberLazyListState() // During selection mode bottom nav is not visible val contentPaddingWithNavBar = contentPadding + if (presenter.selectionMode) { PaddingValues() } else { bottomNavPaddingValues } val scope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing = 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 } }, swipeEnabled = presenter.selectionMode.not(), indicatorPadding = contentPaddingWithNavBar, indicator = { s, trigger -> SwipeRefreshIndicator( state = s, refreshTriggerDistance = trigger, ) }, ) { if (presenter.uiModels.isEmpty()) { EmptyScreen(textResource = R.string.information_no_recent) } else { VerticalFastScroller( listState = updatesListState, topContentPadding = contentPaddingWithNavBar.calculateTopPadding(), endContentPadding = contentPaddingWithNavBar.calculateEndPadding(LocalLayoutDirection.current), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), state = updatesListState, contentPadding = contentPaddingWithNavBar, ) { 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.deleteChapters(dialog.toDelete) presenter.toggleAllSelection(false) }, ) } 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() }