diff --git a/app/build.gradle.kts b/app/build.gradle.kts index acc1db0..c22e505 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,7 @@ apply( dependencies { implementation(libs.androidx.runtime.livedata) + implementation(libs.material) val roomVersion = "2.5.2" diff --git a/app/src/main/java/fr/univpau/queezer/data/Filter.kt b/app/src/main/java/fr/univpau/queezer/data/Filter.kt index c91bfb9..70b3afa 100644 --- a/app/src/main/java/fr/univpau/queezer/data/Filter.kt +++ b/app/src/main/java/fr/univpau/queezer/data/Filter.kt @@ -1,23 +1,32 @@ package fr.univpau.queezer.data +import android.util.Log + data class Filter( - val date: DateFilter = DateFilter.DESCENDING, - val mode : List = listOf(GameMode.TITLE, GameMode.ARTIST, GameMode.ALL), - val nbTitle : Int? = null, + var dateOrderIsAscending: Boolean = true, + + val mode : Map = mapOf( + GameMode.TITLE to true, + GameMode.ARTIST to true, + GameMode.ALL to true + ), + + val nbTitleIsAscending : Boolean = true, ) fun filterGames(filter: Filter, games: List): List { - return games.filter { game -> - 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 - } - } -} + Log.i("Filter", "Filtering games with $filter") -enum class DateFilter { - ASCENDING, - DESCENDING + return games + .filter { game -> + // Filtrer uniquement par les modes de jeu activés + filter.mode.filter { it.value }.keys.contains(game.settings.gameMode) + } + .sortedWith(compareBy { 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) + }) } \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/data/Game.kt b/app/src/main/java/fr/univpau/queezer/data/Game.kt index 2e3791c..24a6ce3 100644 --- a/app/src/main/java/fr/univpau/queezer/data/Game.kt +++ b/app/src/main/java/fr/univpau/queezer/data/Game.kt @@ -11,4 +11,4 @@ data class Game( val playlist: Playlist = Playlist(), val score: Int = 0, val date: Date = Date() -) +) \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/view/components/GameCardItem.kt b/app/src/main/java/fr/univpau/queezer/view/components/GameCardItem.kt index a2c41cf..96a218d 100644 --- a/app/src/main/java/fr/univpau/queezer/view/components/GameCardItem.kt +++ b/app/src/main/java/fr/univpau/queezer/view/components/GameCardItem.kt @@ -1,10 +1,13 @@ package fr.univpau.queezer.view.components +import androidx.compose.foundation.background 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.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -15,9 +18,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import fr.univpau.queezer.R import fr.univpau.queezer.data.Game import fr.univpau.queezer.data.GameMode import java.text.SimpleDateFormat @@ -26,7 +31,8 @@ import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable 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 coroutineScope = rememberCoroutineScope() @@ -36,6 +42,8 @@ fun GameCardItem(game: Game) { (game.settings.numberOfTitles ?: 1) } + val gameModesLabels = context.resources.getStringArray(R.array.game_modes) + Card( onClick = { showBottomSheet.value = true }, modifier = Modifier.fillMaxWidth() @@ -43,14 +51,29 @@ fun GameCardItem(game: Game) { Column( modifier = Modifier.padding(16.dp) ) { - Text( - text = game.playlist.title, - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold - ) - Text(text = formatter.format(game.date)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + 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, style = MaterialTheme.typography.titleMedium ) - Text( - text = "${game.score}/$maxScore", - style = MaterialTheme.typography.titleMedium + Badge( + containerColor = MaterialTheme.colorScheme.primary, + 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) } diff --git a/app/src/main/java/fr/univpau/queezer/view/screens/ScoreScreen.kt b/app/src/main/java/fr/univpau/queezer/view/screens/ScoreScreen.kt index 91dda65..ae470fb 100644 --- a/app/src/main/java/fr/univpau/queezer/view/screens/ScoreScreen.kt +++ b/app/src/main/java/fr/univpau/queezer/view/screens/ScoreScreen.kt @@ -3,13 +3,21 @@ package fr.univpau.queezer.view.screens import android.util.Log import androidx.compose.foundation.layout.Arrangement 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.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons 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.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -18,9 +26,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults 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.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 @@ -31,22 +42,23 @@ import androidx.navigation.NavHostController import fr.univpau.queezer.R import fr.univpau.queezer.data.Filter import fr.univpau.queezer.data.Game +import fr.univpau.queezer.data.GameMode import fr.univpau.queezer.data.filterGames import fr.univpau.queezer.view.components.GameCardItemList 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 fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) { val context = LocalContext.current - val filter = remember { mutableStateOf(Filter()) } + var filter by remember { mutableStateOf(Filter()) } val games: List = gameViewModel.games.observeAsState().value ?: emptyList() - val nbGames = games.size; - val averageSuccessRate = games.sumOf { it.score }.div(max(games.size, 1)) - - val filteredGames = filterGames(filter.value, games) + val filteredGames by remember(filter, games) { derivedStateOf { filterGames(filter, games) } } + val averageSuccessRate by remember(filteredGames) { derivedStateOf { calculateAverageSuccessRate(filteredGames) } } + val nbGames by remember(filteredGames) { derivedStateOf { filteredGames.size } } Scaffold( topBar = { @@ -89,7 +101,7 @@ fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) 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 = "${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) } } @@ -98,6 +110,89 @@ fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) // 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()) { Column ( modifier = Modifier.fillMaxWidth(), @@ -110,4 +205,18 @@ fun ScoreScreen(navController: NavHostController, gameViewModel: GameViewModel) } } } +} + +fun calculateAverageSuccessRate(games: List): 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 } \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/view/screens/SettingsScreen.kt b/app/src/main/java/fr/univpau/queezer/view/screens/SettingsScreen.kt index e254d51..8862b64 100644 --- a/app/src/main/java/fr/univpau/queezer/view/screens/SettingsScreen.kt +++ b/app/src/main/java/fr/univpau/queezer/view/screens/SettingsScreen.kt @@ -82,6 +82,9 @@ fun SettingsScreen(navController: NavHostController, saveLocation: String = "set settings.value.validate(context) saveSettings(context, settings.value, saveLocation) navController.popBackStack() + if (saveLocation != "settings") { + navController.navigate("custom_game") + } } catch (e: Exception) { Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6cb7ab..0a3c62c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ roomPaging = "2.6.1" roomRuntime = "2.6.1" roomRuntimeVersion = "2.5.0" runtimeLivedata = "1.7.6" +material = "1.12.0" [libraries] 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-ktx = { module = "androidx.room:room-ktx" } 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] android-application = { id = "com.android.application", version.ref = "agp" }