4 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
crsmthw f70e082104 v3.0.1 - Minor bug fixes 2026-05-17 10:10:53 +03:00
11 changed files with 72 additions and 44 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/result.png" width="200" />
<img src="assets/screenshots/leaderboard.png" width="200" />
<img src="assets/screenshots/custom.png" width="200" />
</p>
### 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"
minSdk = 35
targetSdk = 37
versionCode = 4
versionName = "3.0.0"
versionCode = 7
versionName = "3.0.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 4,
"versionName": "3.0.0",
"versionCode": 7,
"versionName": "3.0.3",
"outputFile": "app-release.apk"
}
],
@@ -2,8 +2,11 @@ package com.crsmthw.phase10tracker.ui
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.*
import androidx.navigation.navArgument
@@ -27,6 +30,25 @@ object Routes {
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
fun Phase10NavHost(
navController: NavHostController,
@@ -57,12 +79,12 @@ fun Phase10NavHost(
amoledBlack = amoledBlack,
onThemeModeChange = themeVm::setThemeMode,
onAmoledBlackChange = themeVm::setAmoledBlack,
onContinueGame = { gameId -> navController.navigate(Routes.activeGame(gameId)) },
onStartNew = { navController.navigate(Routes.GAME_SETUP) },
onLeaderboard = { navController.navigate(Routes.LEADERBOARD) },
onManagePlayers = { navController.navigate(Routes.PLAYER_ROSTER) },
onCustomRules = { navController.navigate(Routes.CUSTOM_RULES) },
onAbout = { navController.navigate(Routes.ABOUT) }
onContinueGame = { gameId -> navController.navigateSafe(Routes.activeGame(gameId)) },
onStartNew = { navController.navigateSafe(Routes.GAME_SETUP) },
onLeaderboard = { navController.navigateSafe(Routes.LEADERBOARD) },
onManagePlayers = { navController.navigateSafe(Routes.PLAYER_ROSTER) },
onCustomRules = { navController.navigateSafe(Routes.CUSTOM_RULES) },
onAbout = { navController.navigateSafe(Routes.ABOUT) }
)
}
@@ -70,7 +92,7 @@ fun Phase10NavHost(
val vm: PlayerRosterViewModel = viewModel(factory = factory)
PlayerRosterScreen(
vm = vm,
onBack = { navController.popBackStack() }
onBack = { navController.popSafe() }
)
}
@@ -79,11 +101,11 @@ fun Phase10NavHost(
GameSetupScreen(
vm = vm,
onGameStarted = { gameId ->
navController.navigate(Routes.activeGame(gameId)) {
navController.navigateSafe(Routes.activeGame(gameId)) {
popUpTo(Routes.HOME)
}
},
onBack = { navController.popBackStack() }
onBack = { navController.popSafe() }
)
}
@@ -96,18 +118,18 @@ fun Phase10NavHost(
val vm: ActiveGameViewModel = viewModel(factory = gameFactory)
ActiveGameScreen(
vm = vm,
onEnterRound = { navController.navigate(Routes.roundEntry(gameId)) },
onEnterRound = { navController.navigateSafe(Routes.roundEntry(gameId)) },
onGameEnd = {
navController.navigate(Routes.gameResults(gameId)) {
navController.navigateSafe(Routes.gameResults(gameId)) {
popUpTo(Routes.HOME)
}
},
onGameCancelled = {
navController.navigate(Routes.HOME) {
navController.navigateSafe(Routes.HOME) {
popUpTo(Routes.HOME) { inclusive = true }
}
},
onBack = { navController.popBackStack() }
onBack = { navController.popSafe() }
)
}
@@ -120,8 +142,8 @@ fun Phase10NavHost(
val vm: RoundEntryViewModel = viewModel(factory = gameFactory)
RoundEntryScreen(
vm = vm,
onRoundSubmitted = { navController.popBackStack() },
onBack = { navController.popBackStack() }
onRoundSubmitted = { navController.popSafe() },
onBack = { navController.popSafe() }
)
}
@@ -135,7 +157,7 @@ fun Phase10NavHost(
GameResultsScreen(
vm = vm,
onHome = {
navController.navigate(Routes.HOME) {
navController.navigateSafe(Routes.HOME) {
popUpTo(Routes.HOME) { inclusive = true }
}
}
@@ -146,7 +168,7 @@ fun Phase10NavHost(
val vm: LeaderboardViewModel = viewModel(factory = factory)
LeaderboardScreen(
vm = vm,
onBack = { navController.popBackStack() }
onBack = { navController.popSafe() }
)
}
@@ -154,12 +176,12 @@ fun Phase10NavHost(
val vm: CustomPhasesViewModel = viewModel(factory = factory)
CustomPhasesScreen(
vm = vm,
onBack = { navController.popBackStack() }
onBack = { navController.popSafe() }
)
}
composable(Routes.ABOUT) {
AboutScreen(onBack = { navController.popBackStack() })
AboutScreen(onBack = { navController.popSafe() })
}
}
}
@@ -273,7 +273,7 @@ fun GameSetupScreen(
modifier = Modifier.padding(bottom = 4.dp)
)
Text(
"Hold the handle and drag to reorder. First player is the initial dealer.",
"Hold the = handle and drag to reorder. First player is the initial dealer.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -61,6 +61,7 @@ fun RoundEntryScreen(
},
bottomBar = {
Surface(tonalElevation = 3.dp, shadowElevation = 8.dp) {
Column(modifier = Modifier.navigationBarsPadding()) {
Button(
onClick = {
focusManager.clearFocus()
@@ -70,8 +71,7 @@ fun RoundEntryScreen(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(top = 16.dp, bottom = 16.dp)
.navigationBarsPadding()
.padding(vertical = 16.dp)
.height(56.dp),
shape = MaterialTheme.shapes.large
) {
@@ -81,12 +81,17 @@ fun RoundEntryScreen(
}
}
}
}
) { padding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.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(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
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