244 lines
8.3 KiB
Kotlin
244 lines
8.3 KiB
Kotlin
package com.livrets.ui
|
|
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.focusable
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.WindowInsets
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.safeDrawing
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material3.AlertDialog
|
|
import androidx.compose.material3.ElevatedButton
|
|
import androidx.compose.material3.LinearProgressIndicator
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.backhandler.BackHandler
|
|
import androidx.compose.ui.focus.FocusRequester
|
|
import androidx.compose.ui.focus.focusRequester
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.input.key.key
|
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
|
import androidx.compose.ui.input.key.type
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import com.livrets.game.GameConfig
|
|
import com.livrets.game.GameEngine
|
|
import com.livrets.game.GameResult
|
|
import com.livrets.game.GameState
|
|
|
|
@Composable
|
|
fun GameScreen(
|
|
config: GameConfig,
|
|
onFinish: (GameResult) -> Unit,
|
|
onQuit: () -> Unit,
|
|
) {
|
|
val scope = rememberCoroutineScope()
|
|
val engine = remember(config) { GameEngine(config, scope, onFinish = onFinish) }
|
|
val state by engine.state.collectAsState()
|
|
|
|
LaunchedEffect(engine) { engine.start() }
|
|
|
|
var showQuitDialog by remember { mutableStateOf(false) }
|
|
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
|
BackHandler { showQuitDialog = true }
|
|
if (showQuitDialog) {
|
|
AlertDialog(
|
|
onDismissRequest = { showQuitDialog = false },
|
|
title = { Text("Quitter la partie ?") },
|
|
text = { Text("La partie en cours sera abandonnée.") },
|
|
confirmButton = { TextButton(onClick = onQuit) { Text("Quitter") } },
|
|
dismissButton = { TextButton(onClick = { showQuitDialog = false }) { Text("Continuer") } },
|
|
)
|
|
}
|
|
|
|
when (val s = state) {
|
|
is GameState.Playing -> PlayingContent(
|
|
engine = engine,
|
|
state = s,
|
|
progress = s.remainingMs.toFloat() / (config.secondsPerQuestion * 1000L),
|
|
onQuitRequest = { showQuitDialog = true },
|
|
)
|
|
|
|
is GameState.Feedback -> FeedbackContent(s)
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun PlayingContent(
|
|
engine: GameEngine,
|
|
state: GameState.Playing,
|
|
progress: Float,
|
|
onQuitRequest: () -> Unit,
|
|
) {
|
|
val focusRequester = remember { FocusRequester() }
|
|
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
|
.padding(24.dp)
|
|
.focusRequester(focusRequester)
|
|
.focusable()
|
|
.onPreviewKeyEvent { event ->
|
|
when (val action = GameKeyMapping.actionFor(event.key, event.type)) {
|
|
is KeypadAction.Digit -> {
|
|
engine.onDigit(action.char)
|
|
true
|
|
}
|
|
KeypadAction.Delete -> {
|
|
engine.onDelete()
|
|
true
|
|
}
|
|
KeypadAction.Validate -> {
|
|
engine.onValidate()
|
|
true
|
|
}
|
|
null -> false
|
|
}
|
|
},
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
) {
|
|
Text(
|
|
"Question ${state.questionIndex + 1} / ${state.totalQuestions}",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
)
|
|
TextButton(onClick = onQuitRequest) { Text("Quitter") }
|
|
}
|
|
|
|
LinearProgressIndicator(
|
|
progress = { progress.coerceIn(0f, 1f) },
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
|
)
|
|
|
|
Spacer(Modifier.weight(1f))
|
|
|
|
Text(
|
|
text = state.question.toString(),
|
|
style = MaterialTheme.typography.displayMedium,
|
|
textAlign = TextAlign.Center,
|
|
)
|
|
|
|
Spacer(Modifier.height(24.dp))
|
|
|
|
Text(
|
|
text = state.entry.ifEmpty { " " },
|
|
style = MaterialTheme.typography.displayLarge,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(80.dp)
|
|
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp)),
|
|
)
|
|
|
|
Spacer(Modifier.weight(1f))
|
|
|
|
Keypad(
|
|
onDigit = engine::onDigit,
|
|
onDelete = engine::onDelete,
|
|
onValidate = engine::onValidate,
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun Keypad(
|
|
onDigit: (Char) -> Unit,
|
|
onDelete: () -> Unit,
|
|
onValidate: () -> Unit,
|
|
) {
|
|
val rows = listOf(
|
|
listOf("1", "2", "3"),
|
|
listOf("4", "5", "6"),
|
|
listOf("7", "8", "9"),
|
|
listOf("⌫", "0", "OK"),
|
|
)
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
for (row in rows) {
|
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
for (key in row) {
|
|
ElevatedButton(
|
|
onClick = {
|
|
when (key) {
|
|
"⌫" -> onDelete()
|
|
"OK" -> onValidate()
|
|
else -> onDigit(key.first())
|
|
}
|
|
},
|
|
modifier = Modifier.size(width = 96.dp, height = 64.dp),
|
|
) {
|
|
Text(key, fontSize = 24.sp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun FeedbackContent(state: GameState.Feedback) {
|
|
val ok = state.record.isCorrect
|
|
val green = Color(0xFF2E7D32)
|
|
val red = Color(0xFFC62828)
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(if (ok) green.copy(alpha = 0.15f) else red.copy(alpha = 0.15f))
|
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
|
.padding(24.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Center,
|
|
) {
|
|
Text(
|
|
text = if (ok) "✓" else "✗",
|
|
fontSize = 72.sp,
|
|
color = if (ok) green else red,
|
|
)
|
|
Spacer(Modifier.height(16.dp))
|
|
Text(
|
|
text = state.record.question.toString(),
|
|
style = MaterialTheme.typography.headlineMedium,
|
|
textAlign = TextAlign.Center,
|
|
)
|
|
if (!ok) {
|
|
Spacer(Modifier.height(8.dp))
|
|
Text(
|
|
text = if (state.record.givenAnswer == null) "Temps écoulé"
|
|
else "Votre réponse : ${state.record.givenAnswer}",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
color = red,
|
|
)
|
|
Text(
|
|
text = "Bonne réponse : ${state.record.correctAnswer}",
|
|
style = MaterialTheme.typography.titleLarge,
|
|
color = green,
|
|
)
|
|
}
|
|
}
|
|
}
|