feature/swipe-actions #44
@@ -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 =
|
||||||
|
|||||||
@@ -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 = {
|
||||||
viewModel.activateGame(it)
|
scope.launch {
|
||||||
navigateToCalculator()
|
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) },
|
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,23 +211,24 @@ 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)
|
||||||
}
|
}
|
||||||
if (it == DismissValue.DismissedToEnd) {
|
if (it == DismissValue.DismissedToEnd) {
|
||||||
onOpenClicked(game.game.uid)
|
onOpenClicked(game.game.uid)
|
||||||
}
|
}
|
||||||
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, {}, {}, {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user