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