fix: Custom Party + Success rate

This commit is contained in:
Lucàs
2025-01-14 14:25:15 +01:00
parent edcf1b786f
commit 0befbc4ef0
7 changed files with 195 additions and 38 deletions
+1
View File
@@ -46,6 +46,7 @@ apply(
dependencies { dependencies {
implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.runtime.livedata)
implementation(libs.material)
val roomVersion = "2.5.2" val roomVersion = "2.5.2"
@@ -1,23 +1,32 @@
package fr.univpau.queezer.data package fr.univpau.queezer.data
import android.util.Log
data class Filter( data class Filter(
val date: DateFilter = DateFilter.DESCENDING, var dateOrderIsAscending: Boolean = true,
val mode : List<GameMode> = listOf(GameMode.TITLE, GameMode.ARTIST, GameMode.ALL),
val nbTitle : Int? = null, val mode : Map<GameMode, Boolean> = mapOf(
GameMode.TITLE to true,
GameMode.ARTIST to true,
GameMode.ALL to true
),
val nbTitleIsAscending : Boolean = true,
) )
fun filterGames(filter: Filter, games: List<Game>): List<Game> { fun filterGames(filter: Filter, games: List<Game>): List<Game> {
return games.filter { game -> Log.i("Filter", "Filtering games with $filter")
filter.mode.contains(game.settings.gameMode) && (filter.nbTitle == null || game.playlist.tracks.size == filter.nbTitle)
}.sortedBy { game ->
when (filter.date) {
DateFilter.ASCENDING -> game.date.time
DateFilter.DESCENDING -> -game.date.time
}
}
}
enum class DateFilter { return games
ASCENDING, .filter { game ->
DESCENDING // Filtrer uniquement par les modes de jeu activés
filter.mode.filter { it.value }.keys.contains(game.settings.gameMode)
}
.sortedWith(compareBy<Game> { game ->
// Tri par date
if (filter.dateOrderIsAscending) -game.date.time else game.date.time
}.thenBy { game ->
// Tri par nombre de titres
if (filter.nbTitleIsAscending) game.settings.numberOfTitles ?: 0 else -(game.settings.numberOfTitles ?: 0)
})
} }
@@ -11,4 +11,4 @@ data class Game(
val playlist: Playlist = Playlist(), val playlist: Playlist = Playlist(),
val score: Int = 0, val score: Int = 0,
val date: Date = Date() val date: Date = Date()
) )
@@ -1,10 +1,13 @@
package fr.univpau.queezer.view.components package fr.univpau.queezer.view.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -15,9 +18,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import fr.univpau.queezer.R
import fr.univpau.queezer.data.Game import fr.univpau.queezer.data.Game
import fr.univpau.queezer.data.GameMode import fr.univpau.queezer.data.GameMode
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -26,7 +31,8 @@ import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GameCardItem(game: Game) { fun GameCardItem(game: Game) {
val formatter = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()) val context = LocalContext.current
val formatter = SimpleDateFormat("dd MMMM yyyy - HH:mm", Locale.getDefault())
val showBottomSheet = remember { mutableStateOf(false) } val showBottomSheet = remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -36,6 +42,8 @@ fun GameCardItem(game: Game) {
(game.settings.numberOfTitles ?: 1) (game.settings.numberOfTitles ?: 1)
} }
val gameModesLabels = context.resources.getStringArray(R.array.game_modes)
Card( Card(
onClick = { showBottomSheet.value = true }, onClick = { showBottomSheet.value = true },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -43,14 +51,29 @@ fun GameCardItem(game: Game) {
Column( Column(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
Text( Row(
text = game.playlist.title, modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp, horizontalArrangement = Arrangement.SpaceBetween
fontWeight = FontWeight.SemiBold ) {
) Text(
Text(text = formatter.format(game.date)) text = game.playlist.title,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
Text(text = "${game.score}/$maxScore") Badge(
containerColor = MaterialTheme.colorScheme.primary,
content = { Text("${game.score}/${maxScore}") }
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = formatter.format(game.date))
Text(text = "Mode: ${gameModesLabels[game.settings.gameMode.ordinal]}")
}
} }
} }
@@ -76,14 +99,24 @@ fun GameCardItem(game: Game) {
text = game.playlist.title, text = game.playlist.title,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Text( Badge(
text = "${game.score}/$maxScore", containerColor = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleMedium content = {
Text(
text = "${game.score}/${maxScore}pts",
style = MaterialTheme.typography.titleMedium
)
}
) )
} }
Text(
text = formatter.format(game.date), Row(
) modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(formatter.format(game.date))
Text("Mode: ${gameModesLabels[game.settings.gameMode.ordinal]}")
}
TrackCardItemList(game.playlist.tracks) TrackCardItemList(game.playlist.tracks)
} }
@@ -3,13 +3,21 @@ package fr.univpau.queezer.view.screens
import android.util.Log import android.util.Log
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -18,9 +26,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -31,22 +42,23 @@ import androidx.navigation.NavHostController
import fr.univpau.queezer.R import fr.univpau.queezer.R
import fr.univpau.queezer.data.Filter import fr.univpau.queezer.data.Filter
import fr.univpau.queezer.data.Game import fr.univpau.queezer.data.Game
import fr.univpau.queezer.data.GameMode
import fr.univpau.queezer.data.filterGames import fr.univpau.queezer.data.filterGames
import fr.univpau.queezer.view.components.GameCardItemList import fr.univpau.queezer.view.components.GameCardItemList
import fr.univpau.queezer.viewmodel.GameViewModel import fr.univpau.queezer.viewmodel.GameViewModel
import kotlin.math.max import java.util.Locale
import kotlin.math.round
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) { fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) {
val context = LocalContext.current val context = LocalContext.current
val filter = remember { mutableStateOf(Filter()) } var filter by remember { mutableStateOf(Filter()) }
val games: List<Game> = gameViewModel.games.observeAsState().value ?: emptyList() val games: List<Game> = gameViewModel.games.observeAsState().value ?: emptyList()
val nbGames = games.size; val filteredGames by remember(filter, games) { derivedStateOf { filterGames(filter, games) } }
val averageSuccessRate = games.sumOf { it.score }.div(max(games.size, 1)) val averageSuccessRate by remember(filteredGames) { derivedStateOf { calculateAverageSuccessRate(filteredGames) } }
val nbGames by remember(filteredGames) { derivedStateOf { filteredGames.size } }
val filteredGames = filterGames(filter.value, games)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -89,7 +101,7 @@ fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel)
Text(text = "Parties jouées", fontSize = 14.sp) Text(text = "Parties jouées", fontSize = 14.sp)
} }
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "$averageSuccessRate%", fontSize = 24.sp, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) Text(text = "${String.format(Locale.getDefault(), "%.02f", averageSuccessRate)}%", fontSize = 24.sp, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold)
Text(text = "De réussite", fontSize = 14.sp) Text(text = "De réussite", fontSize = 14.sp)
} }
} }
@@ -98,6 +110,89 @@ fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel)
// Todo add filters // Todo add filters
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = true,
onClick = {
// Inverser l'ordre de tri
filter = filter.copy(dateOrderIsAscending = !filter.dateOrderIsAscending)
},
label = { Text("Date") },
leadingIcon = {
if (filter.dateOrderIsAscending) {
Icon(
imageVector = Icons.Filled.KeyboardArrowUp,
contentDescription = "Tri Ascendant",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
} else {
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = "Tri Descendant",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
},
)
val gameModesLabels = context.resources.getStringArray(R.array.game_modes)
filter.mode.entries.forEachIndexed { index, entry ->
FilterChip(
selected = entry.value,
onClick = {
filter = filter.copy(mode = filter.mode.toMutableMap().apply {
this[entry.key] = !entry.value
})
},
leadingIcon = {
if (entry.value) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Filtre activé",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
},
label = { Text(
gameModesLabels[index],
maxLines = 1,
) },
)
}
// Filtrer par nombre de titres
FilterChip(
selected = true,
onClick = {
// Inverser l'ordre de tri
filter = filter.copy(nbTitleIsAscending = !filter.nbTitleIsAscending)
},
label = { Text("Nombre de titres") },
leadingIcon = {
if (filter.nbTitleIsAscending) {
Icon(
imageVector = Icons.Filled.KeyboardArrowUp,
contentDescription = "Tri Ascendant",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
} else {
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = "Tri Descendant",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
},
)
}
if (filteredGames.isEmpty()) { if (filteredGames.isEmpty()) {
Column ( Column (
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -110,4 +205,18 @@ fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel)
} }
} }
} }
}
fun calculateAverageSuccessRate(games: List<Game>): Double {
if (games.isEmpty()) return 0.0
val averageGameScore = games.sumOf {
if (it.settings.gameMode == GameMode.ALL) {
it.score.toDouble() / (it.settings.numberOfTitles!! * 2)
} else {
it.score.toDouble() / (it.settings.numberOfTitles ?: 1)
}
}
return (averageGameScore / games.size) * 100
} }
@@ -82,6 +82,9 @@ fun SettingsScreen(navController: NavHostController, saveLocation: String = "set
settings.value.validate(context) settings.value.validate(context)
saveSettings(context, settings.value, saveLocation) saveSettings(context, settings.value, saveLocation)
navController.popBackStack() navController.popBackStack()
if (saveLocation != "settings") {
navController.navigate("custom_game")
}
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
} }
+2
View File
@@ -21,6 +21,7 @@ roomPaging = "2.6.1"
roomRuntime = "2.6.1" roomRuntime = "2.6.1"
roomRuntimeVersion = "2.5.0" roomRuntimeVersion = "2.5.0"
runtimeLivedata = "1.7.6" runtimeLivedata = "1.7.6"
material = "1.12.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -54,6 +55,7 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
room-compiler = { module = "androidx.room:room-compiler" } room-compiler = { module = "androidx.room:room-compiler" }
room-ktx = { module = "androidx.room:room-ktx" } room-ktx = { module = "androidx.room:room-ktx" }
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }