diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index fdb1e87..cb9e40b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -23,6 +23,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -180,8 +182,11 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { ) { var topBarState by remember { mutableStateOf(TopBarState()) } + var snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } + val scope = rememberCoroutineScope() Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { if (showFab) { FloatingActionButton( @@ -245,7 +250,10 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { topBarState = TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } } - HistoryList(historyViewModel) { navController.navigate(Route.COUNTER) } + HistoryList( + historyViewModel, + snackbarHostState + ) { navController.navigate(Route.COUNTER) } } composable(Route.SETTINGS) { topBarState = diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index 97ae917..a8e4ddb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -3,6 +3,7 @@ package me.zobrist.tichucounter.ui.history import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -13,7 +14,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteForever @@ -27,6 +30,9 @@ import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -35,6 +41,7 @@ import androidx.compose.runtime.Composable 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.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -46,6 +53,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import me.zobrist.tichucounter.R import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.entity.Game @@ -58,11 +66,15 @@ import java.util.Locale @Composable fun HistoryList( - viewModel: HistoryViewModel, navigateToCalculator: () -> Unit + viewModel: HistoryViewModel, + snackbarHostState: SnackbarHostState, + navigateToCalculator: () -> Unit, ) { - + val scope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() var showDeleteDialog by remember { mutableStateOf(false) } + DeleteConfirmDialog(showDeleteDialog) { showDeleteDialog = false if (it) { @@ -70,14 +82,48 @@ fun HistoryList( } } + val deletedMessage = stringResource(id = R.string.delete_success) + val deletedActionLabel = stringResource(id = R.string.undo_question) + + val activatedMessage = stringResource(id = R.string.activated_success) + val activatedActionLabel = stringResource(id = R.string.to_calculator_question) + HistoryList( - viewModel.gameAndHistory, - { - viewModel.activateGame(it) - navigateToCalculator() + games = viewModel.gameAndHistory, + onOpenClicked = { + scope.launch { + viewModel.activateGame(it) + lazyListState.animateScrollToItem(0) + + val result = snackbarHostState.showSnackbar( + message = activatedMessage, + actionLabel = activatedActionLabel, + duration = SnackbarDuration.Short + ) + + if (result == SnackbarResult.ActionPerformed) { + navigateToCalculator() + } + } }, - { viewModel.deleteGame(it) }, - { showDeleteDialog = true }, + onDeleteClicked = { + scope.launch { + viewModel.markToDelete(it) + val result = snackbarHostState.showSnackbar( + message = deletedMessage, + actionLabel = deletedActionLabel, + duration = SnackbarDuration.Short + ) + + if (result == SnackbarResult.Dismissed) { + viewModel.deleteGame(it) + } else { + viewModel.unmarkToDelete(it) + } + } + }, + onDeleteAllClicked = { showDeleteDialog = true }, + lazyListState = lazyListState ) } @@ -104,16 +150,17 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {} } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun HistoryList( games: List, onOpenClicked: (gameId: Long) -> Unit, onDeleteClicked: (gameId: Long) -> Unit, - onDeleteAllClicked: () -> Unit - + onDeleteAllClicked: () -> Unit, + lazyListState: LazyListState = LazyListState(), ) { Row { - LazyColumn { + LazyColumn(state = lazyListState) { item { Text( modifier = Modifier.padding(start = 10.dp, end = 10.dp), @@ -122,7 +169,7 @@ fun HistoryList( ) } items(games.filter { it.game.active }) { - HistoryListItem(it) + HistoryListItem(it, Modifier.animateItemPlacement()) } if (games.count() > 1) { @@ -137,7 +184,9 @@ fun HistoryList( items(items = games.filter { !it.game.active }, key = { it.hashCode() }) { - DismissibleHistoryListItem(it, onOpenClicked, onDeleteClicked) + DismissibleHistoryListItem( + it, onOpenClicked, onDeleteClicked, Modifier.animateItemPlacement() + ) } item { @@ -145,7 +194,8 @@ fun HistoryList( modifier = Modifier .padding(start = 4.dp, end = 4.dp, top = 10.dp) .align(CenterVertically) - .fillMaxWidth(), + .fillMaxWidth() + .animateItemPlacement(), onClick = { onDeleteAllClicked() }) { Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null) Text(text = stringResource(id = R.string.deleteAll)) @@ -161,23 +211,24 @@ fun HistoryList( fun DismissibleHistoryListItem( game: GameWithScores, onOpenClicked: (gameId: Long) -> Unit, - onDeleteClicked: (gameId: Long) -> Unit + onDeleteClicked: (gameId: Long) -> Unit, + modifier: Modifier = Modifier, ) { val density = LocalDensity.current - val dismissState = rememberDismissState( - positionalThreshold = { with(density) { 100.dp.toPx() } }, - confirmValueChange = { - if (it == DismissValue.DismissedToStart) { - onDeleteClicked(game.game.uid) - } - if (it == DismissValue.DismissedToEnd) { - onOpenClicked(game.game.uid) - } - true - }) + val dismissState = + rememberDismissState(positionalThreshold = { with(density) { 100.dp.toPx() } }, + confirmValueChange = { + if (it == DismissValue.DismissedToStart) { + onDeleteClicked(game.game.uid) + } + if (it == DismissValue.DismissedToEnd) { + onOpenClicked(game.game.uid) + } + true + }) - SwipeToDismiss(state = dismissState, background = { + SwipeToDismiss(modifier = modifier, state = dismissState, background = { val direction = dismissState.dismissDirection ?: return@SwipeToDismiss val color by animateColorAsState( when (dismissState.targetValue) { @@ -209,8 +260,10 @@ fun DismissibleHistoryListItem( .padding(horizontal = 20.dp), contentAlignment = alignment ) { - Column(verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { Icon( icon, contentDescription = null, modifier = Modifier.scale(scale) ) @@ -224,7 +277,7 @@ fun DismissibleHistoryListItem( @Composable fun HistoryListItem( - game: GameWithScores + game: GameWithScores, modifier: Modifier = Modifier ) { val format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) @@ -240,7 +293,7 @@ fun HistoryListItem( val totalScores = game.getTotalPoints() Card( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(all = 4.dp), colors = cardColor ) { @@ -284,5 +337,5 @@ private fun HistoryListPreview() { Game(false, "TeamA5", "TeamB5", Date(), Date()), listOf(Round(5, 50, 90)) ) ) - HistoryList(tempData, {}, {}) {} + HistoryList(tempData, {}, {}, {}) } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt index ed3f587..9507838 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt @@ -20,16 +20,27 @@ class HistoryViewModel @Inject constructor( var gameAndHistory by mutableStateOf(emptyList()) private set + private var fullList: List = emptyList() + init { viewModelScope.launch { gameRepository.getAllWithRoundFlow().collect { games -> - gameAndHistory = + fullList = games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed() + gameAndHistory = fullList } } } + fun markToDelete(gameId: Long) { + gameAndHistory = fullList.filter { it.game.uid != gameId } + } + + fun unmarkToDelete(gameId: Long) { + gameAndHistory = fullList + } + fun deleteGame(gameId: Long) { viewModelScope.launch { gameRepository.deleteGame(gameId) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 98e1a74..d2138cd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -25,5 +25,9 @@ About Schreib uns Weiterspielen + Spiel gelöscht. + RÜCKGÄNGIG + Spiel aktiviert. + WEITERSPIELEN \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d688695..bf18678 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,4 +29,8 @@ Contact us Play Store Continue + Game deleted. + UNDO + Game activated. + CONTINUE PLAYING \ No newline at end of file