3 Commits

Author SHA1 Message Date
crsmthw d7ccc2b858 v3.0.3 - fixed double back navigation bug 2026-05-17 11:31:34 +03:00
crsmthw 3637a1e236 minor bugfix 2026-05-17 11:12:36 +03:00
crsmthw 84567bebaa new screenshots 2026-05-17 10:26:21 +03:00
10 changed files with 71 additions and 43 deletions
+2 -1
View File
@@ -23,6 +23,7 @@ Every Phase 10 score tracker app on the Play Store falls into one of a few categ
<img src="assets/screenshots/entry.png" width="200" /> <img src="assets/screenshots/entry.png" width="200" />
<img src="assets/screenshots/result.png" width="200" /> <img src="assets/screenshots/result.png" width="200" />
<img src="assets/screenshots/leaderboard.png" width="200" /> <img src="assets/screenshots/leaderboard.png" width="200" />
<img src="assets/screenshots/custom.png" width="200" />
</p> </p>
### Foldable / Tablet — Dual Pane ### Foldable / Tablet — Dual Pane
@@ -146,4 +147,4 @@ MIT. Do whatever you want with it.
--- ---
*Built with Claude — because the Play Store didn't deserve another ad-infested score tracker.* *Built with Claude — because the people didn't deserve another ad-infested score tracker.*
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.crsmthw.phase10tracker" applicationId = "com.crsmthw.phase10tracker"
minSdk = 35 minSdk = 35
targetSdk = 37 targetSdk = 37
versionCode = 5 versionCode = 7
versionName = "3.0.1" versionName = "3.0.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 5, "versionCode": 7,
"versionName": "3.0.1", "versionName": "3.0.3",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],
@@ -2,8 +2,11 @@ package com.crsmthw.phase10tracker.ui
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.* import androidx.navigation.compose.*
import androidx.navigation.navArgument import androidx.navigation.navArgument
@@ -27,6 +30,25 @@ object Routes {
fun gameResults(id: Long) = "results/$id" fun gameResults(id: Long) = "results/$id"
} }
// ── Safe navigation helpers ───────────────────────────────────────────────────
// Guards every nav action behind a RESUMED check so rapid double-taps during
// transition animations can't pop extra destinations and cause a blank screen.
private fun NavController.navigateSafe(
route: String,
builder: NavOptionsBuilder.() -> Unit = {}
) {
if (currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED) {
navigate(route, builder)
}
}
private fun NavController.popSafe() {
if (currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED) {
popBackStack()
}
}
@Composable @Composable
fun Phase10NavHost( fun Phase10NavHost(
navController: NavHostController, navController: NavHostController,
@@ -57,12 +79,12 @@ fun Phase10NavHost(
amoledBlack = amoledBlack, amoledBlack = amoledBlack,
onThemeModeChange = themeVm::setThemeMode, onThemeModeChange = themeVm::setThemeMode,
onAmoledBlackChange = themeVm::setAmoledBlack, onAmoledBlackChange = themeVm::setAmoledBlack,
onContinueGame = { gameId -> navController.navigate(Routes.activeGame(gameId)) }, onContinueGame = { gameId -> navController.navigateSafe(Routes.activeGame(gameId)) },
onStartNew = { navController.navigate(Routes.GAME_SETUP) }, onStartNew = { navController.navigateSafe(Routes.GAME_SETUP) },
onLeaderboard = { navController.navigate(Routes.LEADERBOARD) }, onLeaderboard = { navController.navigateSafe(Routes.LEADERBOARD) },
onManagePlayers = { navController.navigate(Routes.PLAYER_ROSTER) }, onManagePlayers = { navController.navigateSafe(Routes.PLAYER_ROSTER) },
onCustomRules = { navController.navigate(Routes.CUSTOM_RULES) }, onCustomRules = { navController.navigateSafe(Routes.CUSTOM_RULES) },
onAbout = { navController.navigate(Routes.ABOUT) } onAbout = { navController.navigateSafe(Routes.ABOUT) }
) )
} }
@@ -70,7 +92,7 @@ fun Phase10NavHost(
val vm: PlayerRosterViewModel = viewModel(factory = factory) val vm: PlayerRosterViewModel = viewModel(factory = factory)
PlayerRosterScreen( PlayerRosterScreen(
vm = vm, vm = vm,
onBack = { navController.popBackStack() } onBack = { navController.popSafe() }
) )
} }
@@ -79,11 +101,11 @@ fun Phase10NavHost(
GameSetupScreen( GameSetupScreen(
vm = vm, vm = vm,
onGameStarted = { gameId -> onGameStarted = { gameId ->
navController.navigate(Routes.activeGame(gameId)) { navController.navigateSafe(Routes.activeGame(gameId)) {
popUpTo(Routes.HOME) popUpTo(Routes.HOME)
} }
}, },
onBack = { navController.popBackStack() } onBack = { navController.popSafe() }
) )
} }
@@ -96,18 +118,18 @@ fun Phase10NavHost(
val vm: ActiveGameViewModel = viewModel(factory = gameFactory) val vm: ActiveGameViewModel = viewModel(factory = gameFactory)
ActiveGameScreen( ActiveGameScreen(
vm = vm, vm = vm,
onEnterRound = { navController.navigate(Routes.roundEntry(gameId)) }, onEnterRound = { navController.navigateSafe(Routes.roundEntry(gameId)) },
onGameEnd = { onGameEnd = {
navController.navigate(Routes.gameResults(gameId)) { navController.navigateSafe(Routes.gameResults(gameId)) {
popUpTo(Routes.HOME) popUpTo(Routes.HOME)
} }
}, },
onGameCancelled = { onGameCancelled = {
navController.navigate(Routes.HOME) { navController.navigateSafe(Routes.HOME) {
popUpTo(Routes.HOME) { inclusive = true } popUpTo(Routes.HOME) { inclusive = true }
} }
}, },
onBack = { navController.popBackStack() } onBack = { navController.popSafe() }
) )
} }
@@ -120,8 +142,8 @@ fun Phase10NavHost(
val vm: RoundEntryViewModel = viewModel(factory = gameFactory) val vm: RoundEntryViewModel = viewModel(factory = gameFactory)
RoundEntryScreen( RoundEntryScreen(
vm = vm, vm = vm,
onRoundSubmitted = { navController.popBackStack() }, onRoundSubmitted = { navController.popSafe() },
onBack = { navController.popBackStack() } onBack = { navController.popSafe() }
) )
} }
@@ -135,7 +157,7 @@ fun Phase10NavHost(
GameResultsScreen( GameResultsScreen(
vm = vm, vm = vm,
onHome = { onHome = {
navController.navigate(Routes.HOME) { navController.navigateSafe(Routes.HOME) {
popUpTo(Routes.HOME) { inclusive = true } popUpTo(Routes.HOME) { inclusive = true }
} }
} }
@@ -146,7 +168,7 @@ fun Phase10NavHost(
val vm: LeaderboardViewModel = viewModel(factory = factory) val vm: LeaderboardViewModel = viewModel(factory = factory)
LeaderboardScreen( LeaderboardScreen(
vm = vm, vm = vm,
onBack = { navController.popBackStack() } onBack = { navController.popSafe() }
) )
} }
@@ -154,12 +176,12 @@ fun Phase10NavHost(
val vm: CustomPhasesViewModel = viewModel(factory = factory) val vm: CustomPhasesViewModel = viewModel(factory = factory)
CustomPhasesScreen( CustomPhasesScreen(
vm = vm, vm = vm,
onBack = { navController.popBackStack() } onBack = { navController.popSafe() }
) )
} }
composable(Routes.ABOUT) { composable(Routes.ABOUT) {
AboutScreen(onBack = { navController.popBackStack() }) AboutScreen(onBack = { navController.popSafe() })
} }
} }
} }
@@ -61,23 +61,24 @@ fun RoundEntryScreen(
}, },
bottomBar = { bottomBar = {
Surface(tonalElevation = 3.dp, shadowElevation = 8.dp) { Surface(tonalElevation = 3.dp, shadowElevation = 8.dp) {
Button( Column(modifier = Modifier.navigationBarsPadding()) {
onClick = { Button(
focusManager.clearFocus() onClick = {
vm.submitRound() focusManager.clearFocus()
}, vm.submitRound()
enabled = vm.isValid(), },
modifier = Modifier enabled = vm.isValid(),
.fillMaxWidth() modifier = Modifier
.padding(horizontal = 16.dp) .fillMaxWidth()
.padding(top = 16.dp, bottom = 16.dp) .padding(horizontal = 16.dp)
.navigationBarsPadding() .padding(vertical = 16.dp)
.height(56.dp), .height(56.dp),
shape = MaterialTheme.shapes.large shape = MaterialTheme.shapes.large
) { ) {
Icon(Icons.Filled.Check, null) Icon(Icons.Filled.Check, null)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Submit Round", style = MaterialTheme.typography.titleMedium) Text("Submit Round", style = MaterialTheme.typography.titleMedium)
}
} }
} }
} }
@@ -86,7 +87,11 @@ fun RoundEntryScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
// This is the key fix: pushes content up when keyboard appears // Tell Compose the Scaffold padding already consumed those insets,
// so imePadding() only adds the *extra* keyboard space — not the
// full keyboard height on top of the already-padded bottomBar height.
// Without this, the gap = bottomBar height (the double-count).
.consumeWindowInsets(padding)
.imePadding(), .imePadding(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -307,4 +312,4 @@ private fun CardValuesDialog(onDismiss: () -> Unit) {
TextButton(onClick = onDismiss) { Text("Got it") } TextButton(onClick = onDismiss) { Text("Got it") }
} }
) )
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 258 KiB