feat: conserver la sélection de l'accueil entre les lancements

Les steppers de l'accueil vivaient dans des remember simples : la sélection
était perdue à la fin de chaque partie et à chaque redémarrage de l'app.
Remonter l'état dans App() et le persister via un ConfigRepository
(config.json, même pattern que HistoryRepository) sauvegardé à chaque
changement et rechargé au lancement.
This commit is contained in:
herel committed 2026-08-31 23:11:29 +02:00
1 parent ef0e4e582c
commit f8bd0996bd
4 files changed
+108 -17

No files matched your search

@@ -8,6 +8,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.backhandler.BackHandler import androidx.compose.ui.backhandler.BackHandler
import com.livrets.data.ConfigRepository
import com.livrets.data.HistoryRepository import com.livrets.data.HistoryRepository
import com.livrets.game.GameConfig import com.livrets.game.GameConfig
import com.livrets.game.GameResult import com.livrets.game.GameResult
@@ -48,8 +49,14 @@ fun App(
) = AppTheme(onThemeChanged) { ) = AppTheme(onThemeChanged) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val history = remember { HistoryRepository("$filesDirPath/history.json".toPath()) } val history = remember { HistoryRepository("$filesDirPath/history.json".toPath()) }
val configRepository = remember { ConfigRepository("$filesDirPath/config.json".toPath()) }
var screen by remember { mutableStateOf<Screen>(Screen.Home) } var screen by remember { mutableStateOf<Screen>(Screen.Home) }
var playerName by remember { mutableStateOf<String?>(null) } var playerName by remember { mutableStateOf<String?>(null) }
var config by remember { mutableStateOf(GameConfig(10, 5, 12)) }
LaunchedEffect(configRepository) {
configRepository.load()?.let { config = it }
}
LaunchedEffect(gamesServices) { LaunchedEffect(gamesServices) {
gamesServices.signIn() gamesServices.signIn()
@@ -57,7 +64,13 @@ fun App(
} }
val homePreview: @Composable () -> Unit = { val homePreview: @Composable () -> Unit = {
HomeScreen(onPlay = {}, onHistory = {}, onLeaderboard = {}) HomeScreen(
onPlay = {},
onHistory = {},
onLeaderboard = {},
config = GameConfig(10, 5, 12),
onConfigChanged = {},
)
} }
screen.backTarget()?.let { target -> screen.backTarget()?.let { target ->
@@ -67,14 +80,21 @@ fun App(
when (val current = screen) { when (val current = screen) {
Screen.Home -> HomeScreen( Screen.Home -> HomeScreen(
onPlay = { config -> screen = Screen.Game(config) }, onPlay = { screen = Screen.Game(it) },
onHistory = { screen = Screen.History }, onHistory = { screen = Screen.History },
config = config,
onConfigChanged = { updated ->
config = updated
scope.launch {
withContext(NonCancellable) { configRepository.save(updated) }
}
},
playerName = playerName, playerName = playerName,
onLeaderboard = { config -> onLeaderboard = { current ->
scope.launch { scope.launch {
if (gamesServices.isSignedIn() || gamesServices.signIn()) { if (gamesServices.isSignedIn() || gamesServices.signIn()) {
if (playerName == null) playerName = gamesServices.playerDisplayName() if (playerName == null) playerName = gamesServices.playerDisplayName()
config.leaderboardId()?.let { gamesServices.showLeaderboard(it) } current.leaderboardId()?.let { gamesServices.showLeaderboard(it) }
} }
} }
}, },
@@ -0,0 +1,32 @@
package com.livrets.data
import com.livrets.game.GameConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okio.FileSystem
import okio.Path
class ConfigRepository(
private val filePath: Path,
private val fileSystem: FileSystem = systemFileSystem,
) {
private val json = Json { ignoreUnknownKeys = true }
suspend fun load(): GameConfig? = withContext(Dispatchers.Default) {
if (!fileSystem.exists(filePath)) return@withContext null
runCatching {
fileSystem.read(filePath) { readUtf8() }
.let { json.decodeFromString(GameConfig.serializer(), it) }
}.getOrNull()
}
suspend fun save(config: GameConfig) {
withContext(Dispatchers.Default) {
filePath.parent?.let { fileSystem.createDirectories(it) }
fileSystem.write(filePath) {
writeUtf8(json.encodeToString(GameConfig.serializer(), config))
}
}
}
}
@@ -17,10 +17,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -33,12 +29,10 @@ fun HomeScreen(
onPlay: (GameConfig) -> Unit, onPlay: (GameConfig) -> Unit,
onHistory: () -> Unit, onHistory: () -> Unit,
onLeaderboard: (GameConfig) -> Unit, onLeaderboard: (GameConfig) -> Unit,
config: GameConfig,
onConfigChanged: (GameConfig) -> Unit,
playerName: String? = null, playerName: String? = null,
) { ) {
var questionCount by remember { mutableStateOf(10) }
var secondsPerQuestion by remember { mutableStateOf(5) }
var maxTable by remember { mutableStateOf(12) }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -50,20 +44,26 @@ fun HomeScreen(
Text("Livrets", style = MaterialTheme.typography.displayMedium) Text("Livrets", style = MaterialTheme.typography.displayMedium)
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
StepperRow("Nombre de questions", questionCount, 5..50, step = 5) { questionCount = it } StepperRow("Nombre de questions", config.questionCount, 5..50, step = 5) {
StepperRow("Secondes par question", secondsPerQuestion, 3..30, step = 1) { secondsPerQuestion = it } onConfigChanged(config.copy(questionCount = it))
StepperRow("Jusqu'à la table de", maxTable, 1..12, step = 1) { maxTable = it } }
StepperRow("Secondes par question", config.secondsPerQuestion, 3..30, step = 1) {
onConfigChanged(config.copy(secondsPerQuestion = it))
}
StepperRow("Jusqu'à la table de", config.maxTable, 1..12, step = 1) {
onConfigChanged(config.copy(maxTable = it))
}
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
Button( Button(
onClick = { onPlay(GameConfig(questionCount, secondsPerQuestion, maxTable)) }, onClick = { onPlay(config) },
modifier = Modifier.fillMaxWidth().height(56.dp), modifier = Modifier.fillMaxWidth().height(56.dp),
) { ) {
Text("Jouer", fontSize = 20.sp) Text("Jouer", fontSize = 20.sp)
} }
TextButton(onClick = onHistory) { Text("Historique") } TextButton(onClick = onHistory) { Text("Historique") }
TextButton( TextButton(
onClick = { onLeaderboard(GameConfig(questionCount, secondsPerQuestion, maxTable)) }, onClick = { onLeaderboard(config) },
) { Text("Classement") } ) { Text("Classement") }
playerName?.let { name -> playerName?.let { name ->
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
@@ -0,0 +1,39 @@
package com.livrets.data
import com.livrets.game.GameConfig
import kotlinx.coroutines.test.runTest
import okio.fakefilesystem.FakeFileSystem
import okio.Path.Companion.toPath
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class ConfigRepositoryTest {
@Test
fun `chargement nul quand aucun fichier`() = runTest {
val repo = ConfigRepository("/docs/config.json".toPath(), FakeFileSystem())
assertNull(repo.load())
}
@Test
fun `aller-retour sauvegarde puis chargement`() = runTest {
val repo = ConfigRepository("/docs/config.json".toPath(), FakeFileSystem())
val first = GameConfig(5, 3, 8)
repo.save(first)
assertEquals(first, repo.load())
val second = GameConfig(25, 7, 10)
repo.save(second)
assertEquals(second, repo.load())
}
@Test
fun `chargement nul si fichier invalide`() = runTest {
val fs = FakeFileSystem()
fs.createDirectories("/docs".toPath())
fs.write("/docs/config.json".toPath()) { writeUtf8("n'importe quoi") }
val repo = ConfigRepository("/docs/config.json".toPath(), fs)
assertNull(repo.load())
}
}