feature/swipe-actions #44

Merged
fabian merged 6 commits from feature/swipe-actions into develop 2023-08-25 18:19:45 +02:00
5 changed files with 114 additions and 34 deletions
Showing only changes of commit 55a2293b6c - Show all commits

View File

@@ -23,6 +23,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -180,8 +182,11 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
) { ) {
var topBarState by remember { mutableStateOf(TopBarState()) } var topBarState by remember { mutableStateOf(TopBarState()) }
var snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
val scope = rememberCoroutineScope()
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = { floatingActionButton = {
if (showFab) { if (showFab) {
FloatingActionButton( FloatingActionButton(
@@ -245,7 +250,10 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
topBarState = topBarState =
TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } } 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) { composable(Route.SETTINGS) {
topBarState = topBarState =

View File

@@ -3,6 +3,7 @@ package me.zobrist.tichucounter.ui.history
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DeleteForever 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.SwipeToDismiss
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -35,6 +41,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.R import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Game
@@ -58,11 +66,15 @@ import java.util.Locale
@Composable @Composable
fun HistoryList( fun HistoryList(
viewModel: HistoryViewModel, navigateToCalculator: () -> Unit viewModel: HistoryViewModel,
snackbarHostState: SnackbarHostState,
navigateToCalculator: () -> Unit,
) { ) {
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
DeleteConfirmDialog(showDeleteDialog) { DeleteConfirmDialog(showDeleteDialog) {
showDeleteDialog = false showDeleteDialog = false
if (it) { 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( HistoryList(
viewModel.gameAndHistory, games = viewModel.gameAndHistory,
{ onOpenClicked = {
scope.launch {
viewModel.activateGame(it) viewModel.activateGame(it)
lazyListState.animateScrollToItem(0)
val result = snackbarHostState.showSnackbar(
message = activatedMessage,
actionLabel = activatedActionLabel,
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
navigateToCalculator() navigateToCalculator()
}
}
}, },
{ viewModel.deleteGame(it) }, onDeleteClicked = {
{ showDeleteDialog = true }, 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 @Composable
fun HistoryList( fun HistoryList(
games: List<GameWithScores>, games: List<GameWithScores>,
onOpenClicked: (gameId: Long) -> Unit, onOpenClicked: (gameId: Long) -> Unit,
onDeleteClicked: (gameId: Long) -> Unit, onDeleteClicked: (gameId: Long) -> Unit,
onDeleteAllClicked: () -> Unit onDeleteAllClicked: () -> Unit,
lazyListState: LazyListState = LazyListState(),
) { ) {
Row { Row {
LazyColumn { LazyColumn(state = lazyListState) {
item { item {
Text( Text(
modifier = Modifier.padding(start = 10.dp, end = 10.dp), modifier = Modifier.padding(start = 10.dp, end = 10.dp),
@@ -122,7 +169,7 @@ fun HistoryList(
) )
} }
items(games.filter { it.game.active }) { items(games.filter { it.game.active }) {
HistoryListItem(it) HistoryListItem(it, Modifier.animateItemPlacement())
} }
if (games.count() > 1) { if (games.count() > 1) {
@@ -137,7 +184,9 @@ fun HistoryList(
items(items = games.filter { !it.game.active }, key = { items(items = games.filter { !it.game.active }, key = {
it.hashCode() it.hashCode()
}) { }) {
DismissibleHistoryListItem(it, onOpenClicked, onDeleteClicked) DismissibleHistoryListItem(
it, onOpenClicked, onDeleteClicked, Modifier.animateItemPlacement()
)
} }
item { item {
@@ -145,7 +194,8 @@ fun HistoryList(
modifier = Modifier modifier = Modifier
.padding(start = 4.dp, end = 4.dp, top = 10.dp) .padding(start = 4.dp, end = 4.dp, top = 10.dp)
.align(CenterVertically) .align(CenterVertically)
.fillMaxWidth(), .fillMaxWidth()
.animateItemPlacement(),
onClick = { onDeleteAllClicked() }) { onClick = { onDeleteAllClicked() }) {
Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null) Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null)
Text(text = stringResource(id = R.string.deleteAll)) Text(text = stringResource(id = R.string.deleteAll))
@@ -161,12 +211,13 @@ fun HistoryList(
fun DismissibleHistoryListItem( fun DismissibleHistoryListItem(
game: GameWithScores, game: GameWithScores,
onOpenClicked: (gameId: Long) -> Unit, onOpenClicked: (gameId: Long) -> Unit,
onDeleteClicked: (gameId: Long) -> Unit onDeleteClicked: (gameId: Long) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val dismissState = rememberDismissState( val dismissState =
positionalThreshold = { with(density) { 100.dp.toPx() } }, rememberDismissState(positionalThreshold = { with(density) { 100.dp.toPx() } },
confirmValueChange = { confirmValueChange = {
if (it == DismissValue.DismissedToStart) { if (it == DismissValue.DismissedToStart) {
onDeleteClicked(game.game.uid) onDeleteClicked(game.game.uid)
@@ -177,7 +228,7 @@ fun DismissibleHistoryListItem(
true true
}) })
SwipeToDismiss(state = dismissState, background = { SwipeToDismiss(modifier = modifier, state = dismissState, background = {
val direction = dismissState.dismissDirection ?: return@SwipeToDismiss val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState( val color by animateColorAsState(
when (dismissState.targetValue) { when (dismissState.targetValue) {
@@ -209,8 +260,10 @@ fun DismissibleHistoryListItem(
.padding(horizontal = 20.dp), .padding(horizontal = 20.dp),
contentAlignment = alignment contentAlignment = alignment
) { ) {
Column(verticalArrangement = Arrangement.Center, Column(
horizontalAlignment = Alignment.CenterHorizontally) { verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon( Icon(
icon, contentDescription = null, modifier = Modifier.scale(scale) icon, contentDescription = null, modifier = Modifier.scale(scale)
) )
@@ -224,7 +277,7 @@ fun DismissibleHistoryListItem(
@Composable @Composable
fun HistoryListItem( fun HistoryListItem(
game: GameWithScores game: GameWithScores, modifier: Modifier = Modifier
) { ) {
val format = val format =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
@@ -240,7 +293,7 @@ fun HistoryListItem(
val totalScores = game.getTotalPoints() val totalScores = game.getTotalPoints()
Card( Card(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 4.dp), colors = cardColor .padding(all = 4.dp), colors = cardColor
) { ) {
@@ -284,5 +337,5 @@ private fun HistoryListPreview() {
Game(false, "TeamA5", "TeamB5", Date(), Date()), listOf(Round(5, 50, 90)) Game(false, "TeamA5", "TeamB5", Date(), Date()), listOf(Round(5, 50, 90))
) )
) )
HistoryList(tempData, {}, {}) {} HistoryList(tempData, {}, {}, {})
} }

View File

@@ -20,16 +20,27 @@ class HistoryViewModel @Inject constructor(
var gameAndHistory by mutableStateOf(emptyList<GameWithScores>()) var gameAndHistory by mutableStateOf(emptyList<GameWithScores>())
private set private set
private var fullList: List<GameWithScores> = emptyList()
init { init {
viewModelScope.launch { viewModelScope.launch {
gameRepository.getAllWithRoundFlow().collect { games -> gameRepository.getAllWithRoundFlow().collect { games ->
gameAndHistory = fullList =
games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed() 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) { fun deleteGame(gameId: Long) {
viewModelScope.launch { viewModelScope.launch {
gameRepository.deleteGame(gameId) gameRepository.deleteGame(gameId)

View File

@@ -25,5 +25,9 @@
<string name="menu_about">About</string> <string name="menu_about">About</string>
<string name="contact_us">Schreib uns</string> <string name="contact_us">Schreib uns</string>
<string name="continue_play">Weiterspielen</string> <string name="continue_play">Weiterspielen</string>
<string name="delete_success">Spiel gelöscht.</string>
<string name="undo_question">RÜCKGÄNGIG</string>
<string name="activated_success">Spiel aktiviert.</string>
<string name="to_calculator_question">WEITERSPIELEN</string>
</resources> </resources>

View File

@@ -29,4 +29,8 @@
<string name="contact_us">Contact us</string> <string name="contact_us">Contact us</string>
<string name="play_store" translatable="false">Play Store</string> <string name="play_store" translatable="false">Play Store</string>
<string name="continue_play">Continue</string> <string name="continue_play">Continue</string>
<string name="delete_success">Game deleted.</string>
<string name="undo_question">UNDO</string>
<string name="activated_success">Game activated.</string>
<string name="to_calculator_question">CONTINUE PLAYING</string>
</resources> </resources>