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.setValue
import androidx.compose.ui.backhandler.BackHandler
import com.livrets.data.ConfigRepository
import com.livrets.data.HistoryRepository
import com.livrets.game.GameConfig
import com.livrets.game.GameResult
@@ -48,8 +49,14 @@ fun App(
) = AppTheme(onThemeChanged) {
val scope = rememberCoroutineScope()
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 playerName by remember { mutableStateOf<String?>(null) }
var config by remember { mutableStateOf(GameConfig(10, 5, 12)) }
LaunchedEffect(configRepository) {
configRepository.load()?.let { config = it }
}
LaunchedEffect(gamesServices) {
gamesServices.signIn()
@@ -57,7 +64,13 @@ fun App(
}
val homePreview: @Composable () -> Unit = {
HomeScreen(onPlay = {}, onHistory = {}, onLeaderboard = {})
HomeScreen(
onPlay = {},
onHistory = {},
onLeaderboard = {},
config = GameConfig(10, 5, 12),
onConfigChanged = {},
)
}
screen.backTarget()?.let { target ->
@@ -67,14 +80,21 @@ fun App(
when (val current = screen) {
Screen.Home -> HomeScreen(
onPlay = { config -> screen = Screen.Game(config) },
onPlay = { screen = Screen.Game(it) },
onHistory = { screen = Screen.History },
config = config,
onConfigChanged = { updated ->
config = updated
scope.launch {
withContext(NonCancellable) { configRepository.save(updated) }
}
},
playerName = playerName,
onLeaderboard = { config ->
onLeaderboard = { current ->
scope.launch {
if (gamesServices.isSignedIn() || gamesServices.signIn()) {
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.TextButton
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.Modifier
import androidx.compose.ui.text.style.TextAlign
@@ -33,12 +29,10 @@ fun HomeScreen(
onPlay: (GameConfig) -> Unit,
onHistory: () -> Unit,
onLeaderboard: (GameConfig) -> Unit,
config: GameConfig,
onConfigChanged: (GameConfig) -> Unit,
playerName: String? = null,
) {
var questionCount by remember { mutableStateOf(10) }
var secondsPerQuestion by remember { mutableStateOf(5) }
var maxTable by remember { mutableStateOf(12) }
Column(
modifier = Modifier
.fillMaxSize()
@@ -50,20 +44,26 @@ fun HomeScreen(
Text("Livrets", style = MaterialTheme.typography.displayMedium)
Spacer(Modifier.height(32.dp))
StepperRow("Nombre de questions", questionCount, 5..50, step = 5) { questionCount = it }
StepperRow("Secondes par question", secondsPerQuestion, 3..30, step = 1) { secondsPerQuestion = it }
StepperRow("Jusqu'à la table de", maxTable, 1..12, step = 1) { maxTable = it }
StepperRow("Nombre de questions", config.questionCount, 5..50, step = 5) {
onConfigChanged(config.copy(questionCount = 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))
Button(
onClick = { onPlay(GameConfig(questionCount, secondsPerQuestion, maxTable)) },
onClick = { onPlay(config) },
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text("Jouer", fontSize = 20.sp)
}
TextButton(onClick = onHistory) { Text("Historique") }
TextButton(
onClick = { onLeaderboard(GameConfig(questionCount, secondsPerQuestion, maxTable)) },
onClick = { onLeaderboard(config) },
) { Text("Classement") }
playerName?.let { name ->
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())
}
}