diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt index cff48cceb..70cdd7fbe 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -2,6 +2,8 @@ package eu.kanade.presentation.libraryUpdateError import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn @@ -20,19 +22,27 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.ArrowDownward +import androidx.compose.material.icons.outlined.ArrowUpward +import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.FindReplace import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.ripple +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember @@ -44,8 +54,9 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState import kotlinx.coroutines.Job @@ -72,6 +83,21 @@ fun LibraryUpdateErrorScreen( onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, navigateUp: () -> Unit, ) { + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + val enableScrollToTop by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + val enableScrollToBottom by remember { + derivedStateOf { + listState.canScrollForward + } + } + BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) Scaffold( @@ -82,59 +108,32 @@ fun LibraryUpdateErrorScreen( state.items.size, ), actionModeCounter = state.selected.size, - onSelectAll = { onSelectAll(true) }, - onInvertSelection = onInvertSelection, - onCancelActionMode = { onSelectAll(false) }, scrollBehavior = scrollBehavior, - navigateUp = navigateUp, ) }, bottomBar = { - AnimatedVisibility( - visible = state.selected.isNotEmpty(), - enter = expandVertically(expandFrom = Alignment.Bottom), - exit = shrinkVertically(shrinkTowards = Alignment.Bottom), - ) { - val scope = rememberCoroutineScope() - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.large.copy( - bottomEnd = ZeroCornerSize, - bottomStart = ZeroCornerSize, - ), - tonalElevation = 3.dp, - ) { - val haptic = LocalHapticFeedback.current - val confirm = remember { mutableStateListOf(false) } - var resetJob: Job? = remember { null } - val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - (0 until 1).forEach { i -> confirm[i] = i == toConfirmIndex } - resetJob?.cancel() - resetJob = scope.launch { - delay(1.seconds) - if (isActive) confirm[toConfirmIndex] = false - } + LibraryUpdateErrorsBottomBar( + modifier = modifier, + selected = state.selected, + itemCount = state.items.size, + enableScrollToTop = enableScrollToTop, + enableScrollToBottom = enableScrollToBottom, + onMultiMigrateClicked = onMultiMigrateClicked, + onSelectAll = { onSelectAll(true) }, + onInvertSelection = onInvertSelection, + onCancelActionMode = { onSelectAll(false) }, + navigateUp = navigateUp, + scrollToTop = { + scope.launch { + listState.scrollToItem(0) } - Row( - modifier = Modifier - .padding( - WindowInsets.navigationBars - .only(WindowInsetsSides.Bottom) - .asPaddingValues(), - ) - .padding(horizontal = 8.dp, vertical = 12.dp), - ) { - Button( - title = stringResource(MR.strings.migrate), - icon = Icons.Outlined.FindReplace, - toConfirm = confirm[0], - onLongClick = { onLongClickItem(0) }, - onClick = onMultiMigrateClicked, - ) + }, + scrollToBottom = { + scope.launch { + listState.scrollToItem(state.items.size - 1) } - } - } + }, + ) }, ) { paddingValues -> when { @@ -147,6 +146,7 @@ fun LibraryUpdateErrorScreen( else -> { FastScrollLazyColumn( contentPadding = paddingValues, + state = listState, ) { libraryUpdateErrorUiItems( uiModels = state.getUiModel(), @@ -161,16 +161,157 @@ fun LibraryUpdateErrorScreen( } } +@Composable +private fun LibraryUpdateErrorsBottomBar( + modifier: Modifier = Modifier, + selected: List, + itemCount: Int, + enableScrollToTop: Boolean, + enableScrollToBottom: Boolean, + onMultiMigrateClicked: (() -> Unit), + onSelectAll: () -> Unit, + onInvertSelection: () -> Unit, + onCancelActionMode: () -> Unit, + navigateUp: () -> Unit, + scrollToTop: () -> Unit, + scrollToBottom: () -> Unit, +) { + val scope = rememberCoroutineScope() + val animatedElevation by animateDpAsState(if (selected.isNotEmpty()) 3.dp else 0.dp) + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.large.copy( + bottomEnd = ZeroCornerSize, + bottomStart = ZeroCornerSize, + ), + color = MaterialTheme.colorScheme.surfaceColorAtElevation( + elevation = animatedElevation, + ), + ) { + val haptic = LocalHapticFeedback.current + val confirm = remember { mutableStateListOf(false, false, false, false, false, false) } + var resetJob: Job? = remember { null } + val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + (0 until 6).forEach { i -> confirm[i] = i == toConfirmIndex } + resetJob?.cancel() + resetJob = scope.launch { + delay(1.seconds) + if (isActive) confirm[toConfirmIndex] = false + } + } + Row( + modifier = Modifier + .padding( + WindowInsets.navigationBars + .only(WindowInsetsSides.Bottom) + .asPaddingValues(), + ) + .padding(horizontal = 8.dp, vertical = 12.dp), + ) { + if (selected.isNotEmpty()) { + Button( + title = stringResource(MR.strings.action_cancel), + icon = Icons.Outlined.Close, + toConfirm = confirm[0], + onLongClick = { onLongClickItem(0) }, + onClick = onCancelActionMode, + enabled = true, + ) + } else { + Button( + title = androidx.compose.ui.res.stringResource(R.string.abc_action_bar_up_description), + icon = Icons.AutoMirrored.Outlined.ArrowBack, + toConfirm = confirm[0], + onLongClick = { onLongClickItem(0) }, + onClick = navigateUp, + enabled = true, + ) + } + Button( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Outlined.SelectAll, + toConfirm = confirm[1], + onLongClick = { onLongClickItem(1) }, + onClick = if (selected.isEmpty() or (selected.size != itemCount)) { + onSelectAll + } else { + {} + }, + enabled = selected.isEmpty() or (selected.size != itemCount), + ) + Button( + title = stringResource(MR.strings.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + toConfirm = confirm[2], + onLongClick = { onLongClickItem(2) }, + onClick = if (selected.isNotEmpty()) { + onInvertSelection + } else { + {} + }, + enabled = selected.isNotEmpty(), + ) + Button( + title = stringResource(MR.strings.action_scroll_to_top), + icon = Icons.Outlined.ArrowUpward, + toConfirm = confirm[3], + onLongClick = { onLongClickItem(3) }, + onClick = if (enableScrollToTop) { + scrollToTop + } else { + {} + }, + enabled = enableScrollToTop, + ) + Button( + title = stringResource(MR.strings.action_scroll_to_bottom), + icon = Icons.Outlined.ArrowDownward, + toConfirm = confirm[4], + onLongClick = { onLongClickItem(4) }, + onClick = if (enableScrollToBottom) { + scrollToBottom + } else { + {} + }, + enabled = enableScrollToBottom, + ) + Button( + title = stringResource(MR.strings.migrate), + icon = Icons.Outlined.FindReplace, + toConfirm = confirm[5], + onLongClick = { onLongClickItem(5) }, + onClick = if (selected.isNotEmpty()) { + onMultiMigrateClicked + } else { + {} + }, + enabled = selected.isNotEmpty(), + ) + } + } +} + @Composable private fun RowScope.Button( title: String, icon: ImageVector, toConfirm: Boolean, + enabled: Boolean, onLongClick: () -> Unit, onClick: (() -> Unit), content: (@Composable () -> Unit)? = null, ) { val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f) + val animatedColor by animateColorAsState( + if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.38f, + ) + }, + ) Column( modifier = Modifier .size(48.dp) @@ -187,6 +328,7 @@ private fun RowScope.Button( Icon( imageVector = icon, contentDescription = title, + tint = animatedColor, ) AnimatedVisibility( visible = toConfirm, @@ -198,6 +340,7 @@ private fun RowScope.Button( overflow = TextOverflow.Visible, maxLines = 1, style = MaterialTheme.typography.labelSmall, + color = animatedColor, ) } content?.invoke() @@ -209,32 +352,30 @@ private fun LibraryUpdateErrorsAppBar( modifier: Modifier = Modifier, title: String, actionModeCounter: Int, - onSelectAll: () -> Unit, - onInvertSelection: () -> Unit, - onCancelActionMode: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, - navigateUp: () -> Unit, ) { - AppBar( + val isActionMode by remember(actionModeCounter) { + derivedStateOf { actionModeCounter > 0 } + } + + Column( modifier = modifier, - title = title, - scrollBehavior = scrollBehavior, - actionModeCounter = actionModeCounter, - onCancelActionMode = onCancelActionMode, - actionModeActions = { - IconButton(onClick = onSelectAll) { - Icon( - imageVector = Icons.Outlined.SelectAll, - contentDescription = stringResource(MR.strings.action_select_all), - ) - } - IconButton(onClick = onInvertSelection) { - Icon( - imageVector = Icons.Outlined.FlipToBack, - contentDescription = stringResource(MR.strings.action_select_inverse), - ) - } - }, - navigateUp = navigateUp, - ) + ) { + TopAppBar( + title = { + if (isActionMode) { + AppBarTitle("$actionModeCounter selected") + } else { + AppBarTitle(title) + } + }, + actions = {}, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + elevation = if (isActionMode) 3.dp else 0.dp, + ), + ), + scrollBehavior = scrollBehavior, + ) + } } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 216feaf71..c8eb54ee1 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -577,6 +577,8 @@ Library update errors Library update errors (%d) You have no library update errors. + Scroll to top + Scroll to bottom Networking