feat: Add score screen

This commit is contained in:
Lucàs
2025-01-13 17:57:17 +01:00
parent 498517b2cb
commit ffb8234e19
12 changed files with 445 additions and 183 deletions
@@ -1,17 +1,16 @@
package fr.univpau.queezer
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import fr.univpau.queezer.screen.GameScreen
import fr.univpau.queezer.screen.HomeScreen
import fr.univpau.queezer.screen.ScoreScreen
import fr.univpau.queezer.screen.SettingsScreen
import fr.univpau.queezer.view.screens.GameScreen
import fr.univpau.queezer.view.screens.HomeScreen
import fr.univpau.queezer.view.screens.ScoreScreen
import fr.univpau.queezer.view.screens.SettingsScreen
import fr.univpau.queezer.service.DatabaseService
import fr.univpau.queezer.viewmodel.GameViewModel
@@ -32,7 +32,7 @@ class GameManager() {
constructor(settings: Settings, playlist: Playlist, onTick: () -> Unit, databaseService: DatabaseService) : this() {
this.databaseService = databaseService
this.settings = settings
this.playlist = playlist
this.playlist = Playlist(playlist.title, playlist.tracks.subList(0, settings.numberOfTitles!!))
this.countDownManager = CountdownManager(30000L, onTickTimer = onTick, onFinishTimer = { nextTrack() })
@@ -109,12 +109,7 @@ class GameManager() {
fun nextTrack() {
mediaPlayer.release()
if (currentTrackIndex >= playlist.tracks.size - 1) {
return; // TODO vérifier avant meme de lancer la partie si le nombre de titres est suffisant
}
if (currentTrackIndex >= settings.numberOfTitles!! - 1) {
if (currentTrackIndex + 1 >= playlist.tracks.size) {
gameFinished = true
return;
}
@@ -26,7 +26,5 @@ fun loadSettings(context: Context): Settings {
// Si le JSON n'est pas null, le convertir en objet Settings
return if (json != null) {
Gson().fromJson(json, Settings::class.java)
} else {
Settings()
}
} else Settings()
}
@@ -1,164 +0,0 @@
package fr.univpau.queezer.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.Game
import fr.univpau.queezer.viewmodel.GameViewModel
@Composable
fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) {
val context = LocalContext.current
val gameList : List<Game>? = gameViewModel.games.observeAsState().value
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Titre des paramètres
Text(
text = context.resources.getString(R.string.score),
fontSize = 32.sp,
modifier = Modifier.padding(bottom = 32.dp)
)
// Nombre de parties jouées
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = context.resources.getString(R.string.games_played),
fontSize = 24.sp,
)
if (gameList?.isNotEmpty() == true) {
Text(
text = gameList.size.toString(),
fontSize = 24.sp,
)
} else {
Text(
text = "0",
fontSize = 24.sp,
)
}
}
// pourcentage de réussite moyen
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = context.resources.getString(R.string.average_success_rate),
fontSize = 24.sp,
)
if (gameList?.isNotEmpty() == true) {
Text(
text = (gameList.sumOf { it.score }.div(gameList.size).toString()) + "%",
fontSize = 24.sp,
)
} else {
Text(
text = "0%",
fontSize = 24.sp,
)
}
}
// filtres
// - date
// - mode de jeu titre/artiste/les deux
// - nombre de titres
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = context.resources.getString(R.string.filters),
fontSize = 24.sp,
)
Row(
horizontalArrangement = Arrangement.SpaceBetween
){
Button(
onClick = { /* TODO: Filtre par date */ },
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(vertical = 8.dp)
) {
Text(context.resources.getString(R.string.date))
}
Button(
onClick = { /* TODO: Filtre par date */ },
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(vertical = 8.dp)
) {
Text("Mode de jeu")
}
Button(
onClick = { /* TODO: Filtre par date */ },
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(vertical = 8.dp)
) {
Text("Nombre de titres")
}
}
}
// Historique des parties (score, nom)
// - cliquer sur une partie pour voir les détails
gameList?.forEach {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = it.playlist.title,
fontSize = 24.sp,
)
Text(
text = it.score.toString(),
fontSize = 24.sp,
)
}
}
Button(
onClick = { navController.navigate("home") },
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Text(context.resources.getString(R.string.back))
}
}
}
@@ -0,0 +1,92 @@
package fr.univpau.queezer.view.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.univpau.queezer.data.Game
import fr.univpau.queezer.data.GameMode
import java.text.SimpleDateFormat
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GameCardItem(game: Game) {
val formatter = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
val showBottomSheet = remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val maxScore: Int = if (game.settings.gameMode == GameMode.ALL) {
(game.settings.numberOfTitles ?: 1) * 2
} else {
(game.settings.numberOfTitles ?: 1)
}
Card(
onClick = { showBottomSheet.value = true },
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = game.playlist.title,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
Text(text = formatter.format(game.date))
Text(text = "${game.score}/$maxScore")
}
}
// Modal BottomSheet
if (showBottomSheet.value) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet.value = false // Fermer la BottomSheet au clic en dehors
}
) {
// Contenu de la BottomSheet
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = game.playlist.title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${game.score}/$maxScore",
style = MaterialTheme.typography.titleMedium
)
}
Text(
text = formatter.format(game.date),
)
TrackCardItemList(game.playlist.tracks)
}
}
}
}
@@ -0,0 +1,26 @@
package fr.univpau.queezer.view.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import fr.univpau.queezer.data.Game
@Composable
fun GameCardItemList(games: List<Game>) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
for (game in games) GameCardItem(game)
}
}
@@ -0,0 +1,59 @@
package fr.univpau.queezer.view.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.univpau.queezer.data.Answer
import fr.univpau.queezer.data.Track
@Composable
fun TrackCardItem(track: Track) {
val titleColor = when (track.title.answer) {
Answer.CORRECT -> {
MaterialTheme.colorScheme.primary
}
Answer.INCORRECT -> {
MaterialTheme.colorScheme.error
}
else -> {
MaterialTheme.colorScheme.onSurface
}
}
val artistColor = when (track.artist.answer) {
Answer.CORRECT -> {
MaterialTheme.colorScheme.primary
}
Answer.INCORRECT -> {
MaterialTheme.colorScheme.error
}
else -> {
MaterialTheme.colorScheme.onSurface
}
}
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = track.title.value,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = titleColor
)
Text(text = track.artist.value, color = artistColor)
}
}
}
@@ -0,0 +1,21 @@
package fr.univpau.queezer.view.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import fr.univpau.queezer.data.Game
import fr.univpau.queezer.data.Track
@Composable
fun TrackCardItemList(tracks: List<Track>) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
for (track in tracks) TrackCardItem(track)
}
}
@@ -1,4 +1,4 @@
package fr.univpau.queezer.screen
package fr.univpau.queezer.view.screens
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
@@ -49,7 +49,7 @@ fun GameScreen(navController: NavHostController, database: DatabaseService) {
val context = LocalContext.current
val settings: Settings = loadSettings(context)
var gameManager by remember { mutableStateOf(GameManager(settings, Playlist(), {}, database)) }
var gameManager by remember { mutableStateOf(GameManager()) }
var countdown by remember { mutableIntStateOf(30) }
LaunchedEffect(settings.playlistUrl) {
@@ -1,4 +1,4 @@
package fr.univpau.queezer.screen
package fr.univpau.queezer.view.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
@@ -0,0 +1,236 @@
package fr.univpau.queezer.view.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
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.Game
import fr.univpau.queezer.view.components.GameCardItemList
import fr.univpau.queezer.viewmodel.GameViewModel
import kotlin.math.max
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) {
val context = LocalContext.current
val games: List<Game> = gameViewModel.games.observeAsState().value ?: emptyList()
val nbGames = games.size;
val averageSuccessRate = games.sumOf { it.score }.div(max(games.size, 1))
Scaffold(
topBar = {
CenterAlignedTopAppBar(
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = {
Text(
text = context.resources.getString(R.string.score),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = context.resources.getString(R.string.back)
)
}
},
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceAround,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "$nbGames", fontSize = 24.sp, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold)
Text(text = "Parties jouées", fontSize = 14.sp)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "$averageSuccessRate%", fontSize = 24.sp, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold)
Text(text = "De réussite", fontSize = 14.sp)
}
}
HorizontalDivider()
// Todo add filters
GameCardItemList(games)
}
}
// Column(
// modifier = Modifier
// .fillMaxSize()
// .padding(16.dp),
// horizontalAlignment = Alignment.CenterHorizontally,
// verticalArrangement = Arrangement.spacedBy(16.dp)
// ) {
// // Titre des paramètres
// Text(
// text = context.resources.getString(R.string.score),
// fontSize = 32.sp,
// modifier = Modifier.padding(bottom = 32.dp)
// )
//
// // Nombre de parties jouées
// Row(
// modifier = Modifier.fillMaxWidth(),
// horizontalArrangement = Arrangement.SpaceBetween
// ) {
// Text(
// text = context.resources.getString(R.string.games_played),
// fontSize = 24.sp,
// )
// if (gameList?.isNotEmpty() == true) {
// Text(
// text = gameList.size.toString(),
// fontSize = 24.sp,
// )
// } else {
// Text(
// text = "0",
// fontSize = 24.sp,
// )
// }
//
// }
//
// // pourcentage de réussite moyen
// Row(
// modifier = Modifier.fillMaxWidth(),
// horizontalArrangement = Arrangement.SpaceBetween
// ) {
// Text(
// text = context.resources.getString(R.string.average_success_rate),
// fontSize = 24.sp,
// )
// if (gameList?.isNotEmpty() == true) {
// Text(
// text = (gameList.sumOf { it.score }.div(gameList.size).toString()) + "%",
// fontSize = 24.sp,
// )
// } else {
// Text(
// text = "0%",
// fontSize = 24.sp,
// )
// }
// }
// filtres
// - date
// - mode de jeu titre/artiste/les deux
// - nombre de titres
// Row(
// modifier = Modifier.fillMaxWidth(),
// horizontalArrangement = Arrangement.SpaceBetween
// ) {
// Text(
// text = context.resources.getString(R.string.filters),
// fontSize = 24.sp,
// )
//
// Row(
// horizontalArrangement = Arrangement.SpaceBetween
// ){
// Button(
// onClick = { /* TODO: Filtre par date */ },
// shape = RoundedCornerShape(8.dp),
// modifier = Modifier
// .padding(vertical = 8.dp)
// ) {
// Text(context.resources.getString(R.string.date))
// }
//
// Button(
// onClick = { /* TODO: Filtre par date */ },
// shape = RoundedCornerShape(8.dp),
// modifier = Modifier
// .padding(vertical = 8.dp)
// ) {
// Text("Mode de jeu")
// }
//
// Button(
// onClick = { /* TODO: Filtre par date */ },
// shape = RoundedCornerShape(8.dp),
// modifier = Modifier
// .padding(vertical = 8.dp)
// ) {
// Text("Nombre de titres")
// }
// }
// }
// Historique des parties (score, nom)
// - cliquer sur une partie pour voir les détails
// gameList?.forEach {
// Row(
// modifier = Modifier.fillMaxWidth(),
// horizontalArrangement = Arrangement.SpaceBetween
// ) {
// Text(
// text = it.playlist.title,
// fontSize = 24.sp,
// )
// Text(
// text = it.score.toString(),
// fontSize = 24.sp,
// )
// }
// }
// if (gameList !== null) {
// GameList(gameList)
// }
//
// Button(
// onClick = { navController.navigate("home") },
// shape = RoundedCornerShape(8.dp),
// modifier = Modifier
// .fillMaxWidth()
// .padding(vertical = 8.dp)
// ) {
// Text(context.resources.getString(R.string.back))
// }
//}
}
@@ -1,4 +1,4 @@
package fr.univpau.queezer.screen
package fr.univpau.queezer.view.screens
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement