Compare commits
4 Commits
cbcc2c0dbc
...
v3.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
| d7ccc2b858 | |||
| 3637a1e236 | |||
| 84567bebaa | |||
| f70e082104 |
@@ -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.*
|
||||
|
||||
@@ -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.
@@ -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,23 +61,24 @@ fun RoundEntryScreen(
|
||||
},
|
||||
bottomBar = {
|
||||
Surface(tonalElevation = 3.dp, shadowElevation = 8.dp) {
|
||||
Button(
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
vm.submitRound()
|
||||
},
|
||||
enabled = vm.isValid(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp, bottom = 16.dp)
|
||||
.navigationBarsPadding()
|
||||
.height(56.dp),
|
||||
shape = MaterialTheme.shapes.large
|
||||
) {
|
||||
Icon(Icons.Filled.Check, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Submit Round", style = MaterialTheme.typography.titleMedium)
|
||||
Column(modifier = Modifier.navigationBarsPadding()) {
|
||||
Button(
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
vm.submitRound()
|
||||
},
|
||||
enabled = vm.isValid(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(vertical = 16.dp)
|
||||
.height(56.dp),
|
||||
shape = MaterialTheme.shapes.large
|
||||
) {
|
||||
Icon(Icons.Filled.Check, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Submit Round", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +87,11 @@ fun RoundEntryScreen(
|
||||
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)
|
||||
@@ -307,4 +312,4 @@ private fun CardValuesDialog(onDismiss: () -> Unit) {
|
||||
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 |
Reference in New Issue
Block a user