mirror of
https://github.com/mihonapp/mihon.git
synced 2025-02-06 23:35:17 +01:00
LibraryUpdateErrorScreen's bottom UI with scroll to top/bottom buttons
(cherry picked from commit 859ce54474d456232510e21f4f6795af65489be2)
This commit is contained in:
parent
7c638ad452
commit
d61ec31624
@ -2,6 +2,8 @@ package eu.kanade.presentation.libraryUpdateError
|
|||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.core.animateFloatAsState
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
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.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||||
import androidx.compose.material.icons.Icons
|
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.FindReplace
|
||||||
import androidx.compose.material.icons.outlined.FlipToBack
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
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.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.ripple
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.remember
|
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.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
|
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
|
||||||
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState
|
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -72,6 +83,21 @@ fun LibraryUpdateErrorScreen(
|
|||||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
navigateUp: () -> 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) })
|
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -82,59 +108,32 @@ fun LibraryUpdateErrorScreen(
|
|||||||
state.items.size,
|
state.items.size,
|
||||||
),
|
),
|
||||||
actionModeCounter = state.selected.size,
|
actionModeCounter = state.selected.size,
|
||||||
onSelectAll = { onSelectAll(true) },
|
|
||||||
onInvertSelection = onInvertSelection,
|
|
||||||
onCancelActionMode = { onSelectAll(false) },
|
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigateUp = navigateUp,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
AnimatedVisibility(
|
LibraryUpdateErrorsBottomBar(
|
||||||
visible = state.selected.isNotEmpty(),
|
modifier = modifier,
|
||||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
selected = state.selected,
|
||||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
|
itemCount = state.items.size,
|
||||||
) {
|
enableScrollToTop = enableScrollToTop,
|
||||||
val scope = rememberCoroutineScope()
|
enableScrollToBottom = enableScrollToBottom,
|
||||||
Surface(
|
onMultiMigrateClicked = onMultiMigrateClicked,
|
||||||
modifier = modifier,
|
onSelectAll = { onSelectAll(true) },
|
||||||
shape = MaterialTheme.shapes.large.copy(
|
onInvertSelection = onInvertSelection,
|
||||||
bottomEnd = ZeroCornerSize,
|
onCancelActionMode = { onSelectAll(false) },
|
||||||
bottomStart = ZeroCornerSize,
|
navigateUp = navigateUp,
|
||||||
),
|
scrollToTop = {
|
||||||
tonalElevation = 3.dp,
|
scope.launch {
|
||||||
) {
|
listState.scrollToItem(0)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Row(
|
},
|
||||||
modifier = Modifier
|
scrollToBottom = {
|
||||||
.padding(
|
scope.launch {
|
||||||
WindowInsets.navigationBars
|
listState.scrollToItem(state.items.size - 1)
|
||||||
.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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
when {
|
when {
|
||||||
@ -147,6 +146,7 @@ fun LibraryUpdateErrorScreen(
|
|||||||
else -> {
|
else -> {
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
|
state = listState,
|
||||||
) {
|
) {
|
||||||
libraryUpdateErrorUiItems(
|
libraryUpdateErrorUiItems(
|
||||||
uiModels = state.getUiModel(),
|
uiModels = state.getUiModel(),
|
||||||
@ -161,16 +161,157 @@ fun LibraryUpdateErrorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LibraryUpdateErrorsBottomBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: List<LibraryUpdateErrorItem>,
|
||||||
|
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
|
@Composable
|
||||||
private fun RowScope.Button(
|
private fun RowScope.Button(
|
||||||
title: String,
|
title: String,
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
toConfirm: Boolean,
|
toConfirm: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onClick: (() -> Unit),
|
onClick: (() -> Unit),
|
||||||
content: (@Composable () -> Unit)? = null,
|
content: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
@ -187,6 +328,7 @@ private fun RowScope.Button(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = title,
|
contentDescription = title,
|
||||||
|
tint = animatedColor,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = toConfirm,
|
visible = toConfirm,
|
||||||
@ -198,6 +340,7 @@ private fun RowScope.Button(
|
|||||||
overflow = TextOverflow.Visible,
|
overflow = TextOverflow.Visible,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = animatedColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
content?.invoke()
|
content?.invoke()
|
||||||
@ -209,32 +352,30 @@ private fun LibraryUpdateErrorsAppBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String,
|
title: String,
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
onSelectAll: () -> Unit,
|
|
||||||
onInvertSelection: () -> Unit,
|
|
||||||
onCancelActionMode: () -> Unit,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
navigateUp: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
AppBar(
|
val isActionMode by remember(actionModeCounter) {
|
||||||
|
derivedStateOf { actionModeCounter > 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = title,
|
) {
|
||||||
scrollBehavior = scrollBehavior,
|
TopAppBar(
|
||||||
actionModeCounter = actionModeCounter,
|
title = {
|
||||||
onCancelActionMode = onCancelActionMode,
|
if (isActionMode) {
|
||||||
actionModeActions = {
|
AppBarTitle("$actionModeCounter selected")
|
||||||
IconButton(onClick = onSelectAll) {
|
} else {
|
||||||
Icon(
|
AppBarTitle(title)
|
||||||
imageVector = Icons.Outlined.SelectAll,
|
}
|
||||||
contentDescription = stringResource(MR.strings.action_select_all),
|
},
|
||||||
)
|
actions = {},
|
||||||
}
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
IconButton(onClick = onInvertSelection) {
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
Icon(
|
elevation = if (isActionMode) 3.dp else 0.dp,
|
||||||
imageVector = Icons.Outlined.FlipToBack,
|
),
|
||||||
contentDescription = stringResource(MR.strings.action_select_inverse),
|
),
|
||||||
)
|
scrollBehavior = scrollBehavior,
|
||||||
}
|
)
|
||||||
},
|
}
|
||||||
navigateUp = navigateUp,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -577,6 +577,8 @@
|
|||||||
<string name="option_label_library_update_errors">Library update errors</string>
|
<string name="option_label_library_update_errors">Library update errors</string>
|
||||||
<string name="label_library_update_errors">Library update errors (%d)</string>
|
<string name="label_library_update_errors">Library update errors (%d)</string>
|
||||||
<string name="info_empty_library_update_errors">You have no library update errors.</string>
|
<string name="info_empty_library_update_errors">You have no library update errors.</string>
|
||||||
|
<string name="action_scroll_to_top">Scroll to top</string>
|
||||||
|
<string name="action_scroll_to_bottom">Scroll to bottom</string>
|
||||||
|
|
||||||
<!-- Advanced section -->
|
<!-- Advanced section -->
|
||||||
<string name="label_network">Networking</string>
|
<string name="label_network">Networking</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user