Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7ccc2b858 | |||
| 3637a1e236 | |||
| 84567bebaa |
@@ -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.*
|
||||||
|
|||||||
@@ -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.
@@ -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,6 +61,7 @@ fun RoundEntryScreen(
|
|||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Surface(tonalElevation = 3.dp, shadowElevation = 8.dp) {
|
Surface(tonalElevation = 3.dp, shadowElevation = 8.dp) {
|
||||||
|
Column(modifier = Modifier.navigationBarsPadding()) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
@@ -70,8 +71,7 @@ fun RoundEntryScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(top = 16.dp, bottom = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.navigationBarsPadding()
|
|
||||||
.height(56.dp),
|
.height(56.dp),
|
||||||
shape = MaterialTheme.shapes.large
|
shape = MaterialTheme.shapes.large
|
||||||
) {
|
) {
|
||||||
@@ -81,12 +81,17 @@ fun RoundEntryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
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)
|
||||||
|
|||||||
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