diff --git a/sharedUI/src/commonMain/kotlin/com/livrets/App.kt b/sharedUI/src/commonMain/kotlin/com/livrets/App.kt index 959892b..3222dbf 100644 --- a/sharedUI/src/commonMain/kotlin/com/livrets/App.kt +++ b/sharedUI/src/commonMain/kotlin/com/livrets/App.kt @@ -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.Home) } var playerName by remember { mutableStateOf(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) } } } }, diff --git a/sharedUI/src/commonMain/kotlin/com/livrets/data/ConfigRepository.kt b/sharedUI/src/commonMain/kotlin/com/livrets/data/ConfigRepository.kt new file mode 100644 index 0000000..1105bc6 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/com/livrets/data/ConfigRepository.kt @@ -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)) + } + } + } +} diff --git a/sharedUI/src/commonMain/kotlin/com/livrets/ui/HomeScreen.kt b/sharedUI/src/commonMain/kotlin/com/livrets/ui/HomeScreen.kt index 6496a58..5c9e8bd 100644 --- a/sharedUI/src/commonMain/kotlin/com/livrets/ui/HomeScreen.kt +++ b/sharedUI/src/commonMain/kotlin/com/livrets/ui/HomeScreen.kt @@ -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)) diff --git a/sharedUI/src/commonTest/kotlin/com/livrets/data/ConfigRepositoryTest.kt b/sharedUI/src/commonTest/kotlin/com/livrets/data/ConfigRepositoryTest.kt new file mode 100644 index 0000000..a22b52f --- /dev/null +++ b/sharedUI/src/commonTest/kotlin/com/livrets/data/ConfigRepositoryTest.kt @@ -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()) + } +}