refactor: Use data classes for Settings + Game

This commit is contained in:
Lucàs
2025-01-03 18:41:47 +01:00
parent 5884e1d7be
commit 1edd76f618
19 changed files with 410 additions and 282 deletions
+1
View File
@@ -51,6 +51,7 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation("io.coil-kt:coil-compose:2.4.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -1,61 +0,0 @@
package fr.univpau.queezer
import android.util.Log
import androidx.compose.runtime.MutableState
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
data class Track(
val title: String,
val artist: Artist,
val preview: String,
val album: Album
)
data class Artist(
val name: String
)
data class Album(
val cover: String
)
suspend fun fetchTracks(apiUrl: String): List<Track> {
return withContext(Dispatchers.IO) {
try {
val url = URL(apiUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
val response = connection.inputStream.bufferedReader().use { it.readText() }
val json = Gson().fromJson(response, JsonObject::class.java)
val tracksJson = json["tracks"].asJsonObject["data"].toString()
val trackListType = object : TypeToken<List<Track>>() {}.type
Gson().fromJson<List<Track>>(tracksJson, trackListType)
} else {
emptyList()
}
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
}
}
suspend fun displayTracks(apiUrl: String) {
val tracks = fetchTracks(apiUrl)
tracks?.forEach { track ->
Log.d("Track", track.title)
Log.d("Artist", track.artist.name)
Log.d("Preview", track.preview)
Log.d("Album", track.album.cover)
}
}
@@ -1,151 +0,0 @@
package fr.univpau.queezer
import android.os.CountDownTimer
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
@Composable
fun GameScreen(navController: NavHostController) {
val context = LocalContext.current
val loadedSettings = loadSettings(context)
var selectedGameMode by remember { mutableStateOf(loadedSettings.gameMode) }
val numberOfTitles by remember { mutableIntStateOf(loadedSettings.numberOfTitles.toInt()) }
val playlistUrl by remember { mutableStateOf(loadedSettings.playlistUrl) }
var tracks by remember { mutableStateOf(emptyList<Track>()) }
LaunchedEffect(playlistUrl) {
tracks = fetchTracks(playlistUrl)
if (tracks.isEmpty()) {
// Affiche un message d'erreur en toast
Toast.makeText(context, "Impossible de charger les titres, veuillez vérifier la validité de l'URL.", Toast.LENGTH_SHORT).show()
// Retourn à l'écran d'accueil
navController.popBackStack()
}
tracks = tracks.shuffled() // On mélange les titres
Log.i("Tracks", tracks.toString())
}
val score = remember { mutableIntStateOf(0) }
val remainingTitles = remember { mutableIntStateOf(numberOfTitles) } // Exemple avec 5 titres restants
val userInput = remember { mutableStateOf("") }
// val albumCover: Painter = painterResource(id = R.drawable.album_cover) // Remplacez par une ressource valide d'album
val isCoverVisible = remember { mutableStateOf(false) }
val totalTime = 30000L // 30 secondes
var timeLeft by remember { mutableStateOf(totalTime / 1000) }
var currentTrackIndex by remember { mutableIntStateOf(0) }
// Timer de 30 secondes
LaunchedEffect(Unit) {
object : CountDownTimer(totalTime, 1000) { // Tick toutes les secondes
override fun onTick(millisUntilFinished: Long) {
timeLeft = millisUntilFinished / 1000 // Mettre à jour en secondes
}
override fun onFinish() {
timeLeft = 0 // Compte à rebours terminé
currentTrackIndex += 1 // Passer à la chanson suivante
}
}.start()
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Score
Text("Score : ${score.intValue}", fontSize = 24.sp)
// Nombre de titres restants
Text("Titres restants : ${remainingTitles.intValue}", fontSize = 20.sp)
// Timer
Text("Temps restant : $timeLeft s", fontSize = 20.sp)
// Affichage de la couverture de l'album
if (isCoverVisible.value) {
// Image(painter = albumCover, contentDescription = "Cover", modifier = Modifier.fillMaxWidth())
} else {
Text("Couverture cachée", fontSize = 18.sp)
}
// Champ de texte pour entrer la proposition
TextField(
value = userInput.value,
onValueChange = { userInput.value = it },
label = { Text("Titre / Artiste") },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
modifier = Modifier.fillMaxWidth()
)
// Bouton Valider la réponse
Button(
onClick = {
// Logique pour valider la réponse, en vérifiant la casse et en ajustant le score
val correctAnswer = "Titre Correct" // Exemple, il faut remplacer par la bonne réponse
if (userInput.value.trim().equals(correctAnswer, ignoreCase = true)) {
score.value += 1 // Ajouter 10 points pour une bonne réponse
}
remainingTitles.value -= 1
userInput.value = "" // Réinitialiser le champ de texte
// Réinitialiser ou ajuster le timer si nécessaire
}
) {
Text("Valider")
}
// Bouton Passer
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button(
onClick = {
// Logique pour passer la chanson
remainingTitles.value -= 1
// Vous pouvez réinitialiser le timer, ou passer à la chanson suivante
},
) {
Text("Passer")
}
// Bouton Abandonner
Button(
onClick = {
// Logique pour abandonner, peut-être retour à l'écran d'accueil
navController.popBackStack()
},
) {
Text("Abandonner")
}
}
}
}
@@ -4,13 +4,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import fr.univpau.queezer.screen.GameScreen
import fr.univpau.queezer.screen.HomeScreen
import fr.univpau.queezer.screen.SettingsScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -27,14 +26,8 @@ fun QueezerApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController)
}
composable("game") {
GameScreen(navController)
}
composable("settings") {
SettingsScreen(navController)
}
composable("home") { HomeScreen(navController) }
composable("game") { GameScreen(navController) }
composable("settings") { SettingsScreen(navController) }
}
}
@@ -1,28 +0,0 @@
package fr.univpau.queezer
import android.content.Context
import android.content.SharedPreferences
val gameModes = listOf("Titre Uniquement", "Artiste Uniquement", "Titre et Artiste")
fun saveSettings(context: Context, gameMode: String, numberOfTitles: String, playlistUrl: String) {
val sharedPreferences: SharedPreferences =
context.getSharedPreferences("AppSettings", Context.MODE_PRIVATE)
with(sharedPreferences.edit()) {
putString("gameMode", gameMode)
putString("numberOfTitles", numberOfTitles)
putString("playlistUrl", playlistUrl)
apply()
}
}
fun loadSettings(context: Context): Settings {
val sharedPreferences: SharedPreferences =
context.getSharedPreferences("AppSettings", Context.MODE_PRIVATE)
val gameMode = sharedPreferences.getString("gameMode", gameModes[0]) ?: gameModes[0]
val numberOfTitles = sharedPreferences.getString("numberOfTitles", "30") ?: "30"
val playlistUrl = sharedPreferences.getString("playlistUrl", "") ?: ""
return Settings(gameMode, numberOfTitles, playlistUrl)
}
data class Settings(val gameMode: String, val numberOfTitles: String, val playlistUrl: String)
@@ -0,0 +1,5 @@
package fr.univpau.queezer.data
data class Album(
val cover: String
)
@@ -0,0 +1,5 @@
package fr.univpau.queezer.data
data class Artist(
val name: String
)
@@ -0,0 +1,10 @@
package fr.univpau.queezer.data
import java.sql.Date
data class Game(
val settings: Settings,
val tracks: List<Track>,
val score: Int,
val date: Date
)
@@ -0,0 +1,7 @@
package fr.univpau.queezer.data
enum class GameMode {
TITLE,
ARTIST,
ALL
}
@@ -0,0 +1,27 @@
package fr.univpau.queezer.data
import fr.univpau.queezer.R
data class Settings(
var gameMode: GameMode = GameMode.TITLE,
var numberOfTitles: Int? = 5,
var playlistUrl: String = "https://api.deezer.com/playlist/13279914183",
) {
fun validate(context: android.content.Context) {
if (playlistUrl.isEmpty()) {
throw IllegalArgumentException(context.resources.getString(R.string.error_playlist_url_empty))
}
if (!playlistUrl.startsWith("https://api.deezer.com/playlist/")) {
throw IllegalArgumentException(context.resources.getString(R.string.error_playlist_url_invalid))
}
if (numberOfTitles == null) {
throw IllegalArgumentException(context.resources.getString(R.string.error_tracks_count_empty))
}
if (numberOfTitles!! <= 0) {
throw IllegalArgumentException(context.resources.getString(R.string.error_tracks_count_negative))
}
}
}
@@ -0,0 +1,8 @@
package fr.univpau.queezer.data
data class Track(
val title: String,
val artist: Artist,
val preview: String,
val album: Album
)
@@ -0,0 +1,31 @@
package fr.univpau.queezer.manager
import android.media.MediaPlayer
class AudioManager {
private var mediaPlayer: MediaPlayer = MediaPlayer()
fun play(url: String) {
mediaPlayer.apply {
setDataSource(url)
prepare()
start()
}
}
fun stop() {
mediaPlayer.release()
}
fun pause() {
mediaPlayer.pause()
}
fun resume() {
mediaPlayer.start()
}
fun isPlaying(): Boolean {
return mediaPlayer.isPlaying
}
}
@@ -0,0 +1,34 @@
package fr.univpau.queezer.manager
import android.os.CountDownTimer
class CountdownManager (val duration: Long, val onFinish: () -> Unit) {
var timeLeft = duration;
var interval = 1000L;
var timer: CountDownTimer? = null
private fun create() {
timer = object : CountDownTimer(duration, interval) {
override fun onTick(millisUntilFinished: Long) {
timeLeft = millisUntilFinished / 1000;
}
override fun onFinish() { onFinish() }
}
}
fun start() {
create()
timer?.start()
}
fun stop() {
timer?.cancel()
timer = null
}
fun restart() {
stop()
start()
}
}
@@ -0,0 +1,71 @@
package fr.univpau.queezer.manager
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import fr.univpau.queezer.data.Settings
import fr.univpau.queezer.data.Track
import java.net.HttpURLConnection
import java.net.URL
class GameManager(var settings: Settings) {
val audioManager = AudioManager()
val countDownManager = CountdownManager(30000L, onFinish = ::nextTrack)
var tracks: List<Track> = mutableListOf()
var currentTrackIndex: Int = 0;
var score = 0
suspend fun loadTracks() {
val url = URL(settings.playlistUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Failed to load tracks")
}
val response = connection.inputStream.bufferedReader().use { it.readText() }
val json = Gson().fromJson(response, JsonObject::class.java)
val tracksJson = json["tracks"].asJsonObject["data"].toString()
// Assurez-vous d'utiliser un TypeToken explicite pour une liste de Track
tracks = Gson().fromJson(tracksJson, object : TypeToken<List<Track>>() {}.type)
}
fun nextTrack() {
// Stop the current track
audioManager.stop()
// Play the next track
if (currentTrackIndex >= tracks.size - 1) {
return
}
currentTrackIndex++
audioManager.play(getCurrentTrack().preview)
// Restart the countdown
countDownManager.restart()
}
fun getCurrentTrack(): Track {
if (tracks.isEmpty()) {
throw IllegalStateException("La liste des pistes est vide")
}
return tracks[currentTrackIndex]
}
fun start() {
if (tracks.isEmpty()) {
throw IllegalStateException("Aucune piste n'a été trouvée dans la playlist")
}
countDownManager.start()
audioManager.play(tracks[currentTrackIndex].preview)
}
}
@@ -0,0 +1,32 @@
package fr.univpau.queezer.manager
import android.content.Context
import com.google.gson.Gson
import fr.univpau.queezer.data.Settings
fun saveSettings(context: Context, settings: Settings) {
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
// Sérialiser l'objet Settings en JSON
val json = Gson().toJson(settings)
// Sauvegarder le JSON dans les SharedPreferences
editor.putString("settings", json)
editor.apply()
}
fun loadSettings(context: Context): Settings {
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
// Récupérer le JSON depuis SharedPreferences
val json = sharedPreferences.getString("settings", null)
// Si le JSON n'est pas null, le convertir en objet Settings
return if (json != null) {
Gson().fromJson(json, Settings::class.java)
} else {
Settings()
}
}
@@ -0,0 +1,104 @@
package fr.univpau.queezer.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
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.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import fr.univpau.queezer.R
import fr.univpau.queezer.data.Settings
import fr.univpau.queezer.manager.loadSettings
import coil.compose.AsyncImage
@Composable
fun GameScreen(navController: NavHostController) {
val context = LocalContext.current
val settings: Settings = loadSettings(context)
// val gameManager: GameManager = remember { GameManager(settings) }
// LaunchedEffect(gameManager) {
// gameManager.loadTracks()
// gameManager.start()
// }
// État de l'utilisateur et des éléments du jeu
val userInput = remember { mutableStateOf("") }
// Affichage de l'interface
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Score : 0", fontSize = 24.sp)
Text("Temps restant : 30sec", fontSize = 20.sp)
// Affiche une image a partir d'une url
AsyncImage(
model = "https://api.deezer.com/album/382921287/image",
contentDescription = "Image from URL",
modifier = Modifier
.width(200.dp)
.height(200.dp)
.blur(30.dp)
,
contentScale = ContentScale.Crop
)
Text("Titre : Légende Vivante", fontSize = 20.sp)
Text("Artiste : Lorenzo", fontSize = 20.sp)
// Champ de texte pour entrer la proposition
TextField(
value = userInput.value,
onValueChange = { userInput.value = it },
label = { Text("Titre / Artiste") },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
modifier = Modifier.fillMaxWidth()
)
// Bouton Valider la réponse
Button(
onClick = {
userInput.value = "" // Réinitialiser le champ de texte
}
) { Text(context.resources.getString(R.string.submit)) }
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button(
onClick = {
userInput.value = "" // Réinitialiser le champ de texte
},
) { Text(context.resources.getString(R.string.skip)) }
Button(onClick = { navController.popBackStack() })
{ Text(context.resources.getString(R.string.give_up)) }
}
}
}
@@ -1,4 +1,4 @@
package fr.univpau.queezer
package fr.univpau.queezer.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -1,5 +1,6 @@
package fr.univpau.queezer
package fr.univpau.queezer.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -13,10 +14,8 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.platform.LocalContext
@@ -24,15 +23,16 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import fr.univpau.queezer.R
import fr.univpau.queezer.data.GameMode
import fr.univpau.queezer.manager.loadSettings
import fr.univpau.queezer.manager.saveSettings
import java.net.URL
@Composable
fun SettingsScreen(navController: NavHostController) {
val context = LocalContext.current
val loadedSettings = loadSettings(context)
var selectedGameMode by remember { mutableStateOf(loadedSettings.gameMode) }
var numberOfTitles by remember { mutableStateOf(loadedSettings.numberOfTitles) }
var playlistUrl by remember { mutableStateOf(loadedSettings.playlistUrl) }
val settings = remember { mutableStateOf(loadSettings(context)) }
Column(
modifier = Modifier
@@ -43,7 +43,7 @@ fun SettingsScreen(navController: NavHostController) {
) {
// Titre des paramètres
Text(
text = "Paramètres",
text = context.resources.getString(R.string.settings),
fontSize = 32.sp,
modifier = Modifier.padding(bottom = 32.dp)
)
@@ -54,10 +54,12 @@ fun SettingsScreen(navController: NavHostController) {
horizontalArrangement = Arrangement.SpaceBetween
) {
TextField(
value = playlistUrl,
onValueChange = { playlistUrl = it },
label = { Text("URL de la playlist") },
modifier = Modifier.fillMaxWidth()
label = { Text(context.resources.getString(R.string.playlist_url_label)) },
placeholder = { Text(context.resources.getString(R.string.playlist_url_hint)) },
modifier = Modifier.fillMaxWidth(),
value = settings.value.playlistUrl,
onValueChange = { settings.value = settings.value.copy(playlistUrl = it) },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri),
)
}
@@ -67,11 +69,13 @@ fun SettingsScreen(navController: NavHostController) {
horizontalArrangement = Arrangement.SpaceBetween
) {
TextField(
value = numberOfTitles,
onValueChange = { numberOfTitles = it },
label = { Text("Nombre de titres dans une partie") },
label = { Text(context.resources.getString(R.string.tracks_count_label)) },
modifier = Modifier.fillMaxWidth(1f),
value = settings.value.numberOfTitles?.toString() ?: "",
onValueChange = {
settings.value = settings.value.copy(numberOfTitles = it.toIntOrNull())
},
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(1f)
)
}
@@ -82,16 +86,20 @@ fun SettingsScreen(navController: NavHostController) {
) {
Column {
Text(text = "Mode de jeu", fontSize = 18.sp)
gameModes.forEach { option ->
val gameModes = context.resources.getStringArray(R.array.game_modes)
gameModes.forEachIndexed { index, label ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 16.dp)
) {
RadioButton(
selected = selectedGameMode == option,
onClick = { selectedGameMode = option }
selected = settings.value.gameMode.ordinal == index,
onClick = {
settings.value =
settings.value.copy(gameMode = GameMode.entries[index])
}
)
Text(text = option)
Text(text = label)
}
}
}
@@ -104,18 +112,21 @@ fun SettingsScreen(navController: NavHostController) {
) {
OutlinedButton(
onClick = { navController.popBackStack() },
) {
Text("Retour")
}
) { Text(context.resources.getString(R.string.back)) }
Button(
onClick = {
saveSettings(context, selectedGameMode, numberOfTitles, playlistUrl)
navController.popBackStack()
try {
settings.value.validate(context)
saveSettings(context, settings.value)
navController.popBackStack()
} catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
},
) {
Text("Valider")
Text(context.resources.getString(R.string.submit))
}
}
}
}
}
+29
View File
@@ -1,3 +1,32 @@
<resources>
<string name="app_name">Queezer</string>
<string name="quick_play">Partie rapide</string>
<string name="custom_play">Partie personnalisée</string>
<string name="settings">Paramètre</string>
<string name="score">Score</string>
<!-- Settings -->
<string name="playlist_url_label">URL de la playlist</string>
<string name="playlist_url_hint">https://api.deezer.com/playlist/...</string>
<string name="error_playlist_url_empty">L\'URL de la playlist ne peut pas être vide</string>
<string name="error_playlist_url_invalid">L\'URL de la playlist est invalide</string>
<string name="tracks_count_label">Nombre de titres dans une partie</string>
<string name="tracks_count_hint">10</string>
<string name="error_tracks_count_empty">Le nombre de titres ne peut pas être vide</string>
<string name="error_tracks_count_negative">Le nombre de titres doit être supérieur à 0</string>
<string name="game_mode_label">Mode de jeu</string>
<string-array name="game_modes">
<item>Titres</item>
<item>Artistes</item>
<item>Tout</item>
</string-array>
<string name="back">Retour</string>
<string name="skip">Passer</string>
<string name="give_up">Abandonner</string>
<string name="submit">Valider</string>
</resources>