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:
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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user