diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index d603dda..eaab86c 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -11,6 +11,9 @@ + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 178f664..acc1db0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + id("kotlin-kapt") } android { @@ -39,7 +40,24 @@ android { } } +apply( + plugin = "kotlin-kapt" +) + dependencies { + implementation(libs.androidx.runtime.livedata) + val roomVersion = "2.5.2" + + + //annotationProcessor(libs.androidx.room.compiler) + implementation("androidx.room:room-common:${roomVersion}") + kapt("androidx.room:room-compiler:${roomVersion}") + implementation("androidx.room:room-runtime:${roomVersion}") + implementation("androidx.room:room-ktx:${roomVersion}") + implementation("androidx.room:room-paging:${roomVersion}") + //implementation(libs.androidx.room.ktx) + + implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -51,7 +69,7 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.retrofit) implementation(libs.converter.gson) - implementation("io.coil-kt:coil-compose:2.4.0") + implementation(libs.coil.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31b2726..a8b0376 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Queezer" tools:targetApi="31"> diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..db70abc Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/fr/univpau/queezer/MainActivity.kt b/app/src/main/java/fr/univpau/queezer/MainActivity.kt index 65ac09f..3ab91c3 100644 --- a/app/src/main/java/fr/univpau/queezer/MainActivity.kt +++ b/app/src/main/java/fr/univpau/queezer/MainActivity.kt @@ -1,6 +1,7 @@ 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 @@ -9,25 +10,31 @@ 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.service.DatabaseService +import fr.univpau.queezer.viewmodel.GameViewModel class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val database: DatabaseService by lazy { DatabaseService.getDatabase(this) } setContent { - QueezerApp() + QueezerApp(database) } } } @Composable -fun QueezerApp() { +fun QueezerApp(database: DatabaseService) { val navController = rememberNavController() + val gameViewModel = GameViewModel(database.gameDao()) NavHost(navController = navController, startDestination = "home") { composable("home") { HomeScreen(navController) } - composable("game") { GameScreen(navController) } + composable("game") { GameScreen(navController, database) } composable("settings") { SettingsScreen(navController) } + composable("score") { ScoreScreen(navController, gameViewModel) } } } \ 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 847cdc0..2e3791c 100644 --- a/app/src/main/java/fr/univpau/queezer/data/Game.kt +++ b/app/src/main/java/fr/univpau/queezer/data/Game.kt @@ -1,10 +1,14 @@ package fr.univpau.queezer.data -import java.sql.Date +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.util.Date +@Entity(tableName = "game") data class Game( - val settings: Settings, - val tracks: List, - val score: Int, - val date: Date + @PrimaryKey(autoGenerate = true) val id: Int, + val settings: Settings = Settings(), + val playlist: Playlist = Playlist(), + val score: Int = 0, + val date: Date = Date() ) diff --git a/app/src/main/java/fr/univpau/queezer/data/GameDao.kt b/app/src/main/java/fr/univpau/queezer/data/GameDao.kt new file mode 100644 index 0000000..ab3f209 --- /dev/null +++ b/app/src/main/java/fr/univpau/queezer/data/GameDao.kt @@ -0,0 +1,17 @@ +package fr.univpau.queezer.data + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface GameDao { + + @Insert + fun insert(game: Game) + + @Query("SELECT * FROM game") + fun getAll(): LiveData> + + @Delete + fun delete(game: Game) +} \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/data/Playlist.kt b/app/src/main/java/fr/univpau/queezer/data/Playlist.kt new file mode 100644 index 0000000..27416e5 --- /dev/null +++ b/app/src/main/java/fr/univpau/queezer/data/Playlist.kt @@ -0,0 +1,6 @@ +package fr.univpau.queezer.data + +data class Playlist( + val title: String = "", + val tracks: List = emptyList() +) diff --git a/app/src/main/java/fr/univpau/queezer/manager/CountdownManager.kt b/app/src/main/java/fr/univpau/queezer/manager/CountdownManager.kt index 6a2ae89..6520f25 100644 --- a/app/src/main/java/fr/univpau/queezer/manager/CountdownManager.kt +++ b/app/src/main/java/fr/univpau/queezer/manager/CountdownManager.kt @@ -2,7 +2,7 @@ package fr.univpau.queezer.manager import android.os.CountDownTimer -class CountdownManager (val duration: Long, val onFinish: () -> Unit) { +class CountdownManager (val duration: Long, val onTickTimer: () -> Unit, val onFinishTimer: () -> Unit) { var timeLeft = duration / 1000; var interval = 1000L; @@ -12,8 +12,11 @@ class CountdownManager (val duration: Long, val onFinish: () -> Unit) { timer = object : CountDownTimer(duration, interval) { override fun onTick(millisUntilFinished: Long) { timeLeft = millisUntilFinished / 1000; + onTickTimer() + } + override fun onFinish() { + onFinishTimer() } - override fun onFinish() { onFinish() } } } diff --git a/app/src/main/java/fr/univpau/queezer/manager/GameManager.kt b/app/src/main/java/fr/univpau/queezer/manager/GameManager.kt index 58df34c..b235c89 100644 --- a/app/src/main/java/fr/univpau/queezer/manager/GameManager.kt +++ b/app/src/main/java/fr/univpau/queezer/manager/GameManager.kt @@ -1,29 +1,42 @@ package fr.univpau.queezer.manager +import android.content.Context import android.media.MediaPlayer import android.util.Log import fr.univpau.queezer.data.Answer +import fr.univpau.queezer.data.Game import fr.univpau.queezer.data.GameMode +import fr.univpau.queezer.data.Playlist import fr.univpau.queezer.data.Settings import fr.univpau.queezer.data.Track +import fr.univpau.queezer.service.DatabaseService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.Date import java.util.Locale class GameManager() { - + private lateinit var databaseService: DatabaseService private var mediaPlayer: MediaPlayer = MediaPlayer() - val countDownManager = CountdownManager(30000L, onFinish = ::nextTrack) + var countDownManager = CountdownManager(30000L, {}, {}) var settings: Settings = Settings() - var tracks: List = emptyList() + // var tracks: List = emptyList() + var playlist: Playlist = Playlist() + var gameFinished : Boolean = false; var currentTrackIndex: Int = 0; var score = 0 - constructor(settings: Settings, tracks: List) : this() { + constructor(settings: Settings, playlist: Playlist, onTick: () -> Unit, databaseService: DatabaseService) : this() { + this.databaseService = databaseService this.settings = settings - this.tracks = tracks + this.playlist = playlist - for (track in tracks) { + this.countDownManager = CountdownManager(30000L, onTickTimer = onTick, onFinishTimer = { nextTrack() }) + + for (track in playlist.tracks) { if (settings.gameMode == GameMode.TITLE) { track.title.answer = Answer.INCORRECT } @@ -74,7 +87,7 @@ class GameManager() { fun formatString(input: String): String { return input .trim() - .toLowerCase(Locale.ROOT) + .lowercase(Locale.ROOT) .removeSurrounding("(", ")") .removeSurrounding("[", "]") .removeSurrounding("{", "}") @@ -84,46 +97,65 @@ class GameManager() { } fun getCurrentTrack(): Track? { - if (tracks.isEmpty()) return null; - return tracks[currentTrackIndex] + if (playlist.tracks.isEmpty()) return null; + return playlist.tracks[currentTrackIndex] } fun stop() { mediaPlayer.release() - // countDownManager.stop() + countDownManager.stop() } fun nextTrack() { mediaPlayer.release() - if (currentTrackIndex >= tracks.size - 1) { + 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) { + gameFinished = true return; } currentTrackIndex++ mediaPlayer = MediaPlayer().apply { - setDataSource(tracks[currentTrackIndex].preview) + setDataSource(playlist.tracks[currentTrackIndex].preview) prepare() start() } - // countDownManager.restart() + countDownManager.restart() } fun start() { - // countDownManager.start() - Log.i("GameManager", "Next track: ${tracks[currentTrackIndex].preview}") + countDownManager.start() + Log.i("GameManager", "Next track: ${playlist.tracks[currentTrackIndex].preview}") mediaPlayer = MediaPlayer().apply { - setDataSource(tracks[currentTrackIndex].preview) + setDataSource(playlist.tracks[currentTrackIndex].preview) prepare() start() } } + + fun save(context: Context) { + // TODO Sauvegarder tout le jeu en base de donnée locale (Room) dans un objet Game + + // Créer un objet Game + val game = Game( + id = 0, + settings = settings, + playlist = playlist, + score = score, + date = Date() + ) + + // Sauvegarder le jeu avec Room + CoroutineScope(Dispatchers.IO).launch { + databaseService.gameDao().insert(game) + } + } } \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/manager/TrackManager.kt b/app/src/main/java/fr/univpau/queezer/manager/TrackManager.kt index 18ba01a..852dd2b 100644 --- a/app/src/main/java/fr/univpau/queezer/manager/TrackManager.kt +++ b/app/src/main/java/fr/univpau/queezer/manager/TrackManager.kt @@ -1,11 +1,38 @@ package fr.univpau.queezer.manager import fr.univpau.queezer.data.Input +import fr.univpau.queezer.data.Playlist import fr.univpau.queezer.data.Track +import fr.univpau.queezer.service.PlaylistResponse import fr.univpau.queezer.service.createDeezerApiService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +suspend fun fetchPlaylist(apiUrl: String) : Playlist? { + val deezerApiService = createDeezerApiService() + + return withContext(Dispatchers.IO) { + try { + val playlistResponse : PlaylistResponse = deezerApiService.getPlaylist(apiUrl) + + Playlist( + title = playlistResponse.title, + tracks = playlistResponse.tracks.data.map { track -> + Track( + preview = track.preview, + album = track.album.cover, + title = Input(value = track.title), + artist = Input(value = track.artist.name) + ) + }.shuffled() + ) + } catch (e: Exception) { + e.printStackTrace() + null; + } + } +} + suspend fun fetchAndFormatPlaylist(apiUrl: String): List { val deezerApiService = createDeezerApiService() diff --git a/app/src/main/java/fr/univpau/queezer/screen/GameScreen.kt b/app/src/main/java/fr/univpau/queezer/screen/GameScreen.kt index 2a35232..56943ff 100644 --- a/app/src/main/java/fr/univpau/queezer/screen/GameScreen.kt +++ b/app/src/main/java/fr/univpau/queezer/screen/GameScreen.kt @@ -18,8 +18,10 @@ 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,22 +37,25 @@ import fr.univpau.queezer.data.Settings import fr.univpau.queezer.manager.loadSettings import coil.compose.AsyncImage import fr.univpau.queezer.data.Answer +import fr.univpau.queezer.data.Playlist import fr.univpau.queezer.manager.GameManager -import fr.univpau.queezer.manager.fetchAndFormatPlaylist +import fr.univpau.queezer.manager.fetchPlaylist +import fr.univpau.queezer.service.DatabaseService +import kotlinx.coroutines.launch @Composable -fun GameScreen(navController: NavHostController) { +fun GameScreen(navController: NavHostController, database: DatabaseService) { + val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val settings: Settings = loadSettings(context) - var gameManager by remember { mutableStateOf(GameManager(settings, emptyList())) } - var currentTrack by remember { mutableStateOf(gameManager.getCurrentTrack()) } + var gameManager by remember { mutableStateOf(GameManager(settings, Playlist(), {}, database)) } + var countdown by remember { mutableIntStateOf(30) } LaunchedEffect(settings.playlistUrl) { - val tracks = fetchAndFormatPlaylist(settings.playlistUrl).shuffled() - gameManager = GameManager(settings, tracks) + val playlist = fetchPlaylist(settings.playlistUrl) + gameManager = GameManager(settings, playlist ?: Playlist(), { countdown = gameManager.countDownManager.timeLeft.toInt() }, database) - currentTrack = gameManager.getCurrentTrack() gameManager.start() Log.i("GameScreen", gameManager.getCurrentTrack().toString()) } @@ -67,21 +72,32 @@ fun GameScreen(navController: NavHostController) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { - if (currentTrack == null) { + if (gameManager.getCurrentTrack() == null) { CircularProgressIndicator( modifier = Modifier.width(64.dp), color = MaterialTheme.colorScheme.secondary, trackColor = MaterialTheme.colorScheme.surfaceVariant, ) + } else if (gameManager.gameFinished) { + Text("Partie terminée !", fontSize = 24.sp) + Text("Score : ${gameManager.score}", fontSize = 20.sp) + Button(onClick = { + + coroutineScope.launch { + gameManager.save(context) + gameManager.stop() + navController.popBackStack() + } + }) { Text(context.resources.getString(R.string.back)) } } else { Text("Score : ${gameManager.score}", fontSize = 24.sp) - Text("Temps restant : ${gameManager.countDownManager.timeLeft}sec", fontSize = 20.sp) + Text("Temps restant : ${countdown}sec", fontSize = 20.sp) - if (currentTrack!!.title.answer == Answer.CORRECT && currentTrack!!.artist.answer == Answer.UNKNOWN - || currentTrack!!.title.answer == Answer.UNKNOWN && currentTrack!!.artist.answer == Answer.CORRECT - || currentTrack!!.title.answer == Answer.CORRECT && currentTrack!!.artist.answer == Answer.CORRECT) { + if (gameManager.getCurrentTrack()!!.title.answer == Answer.CORRECT && gameManager.getCurrentTrack()!!.artist.answer == Answer.UNKNOWN + || gameManager.getCurrentTrack()!!.title.answer == Answer.UNKNOWN && gameManager.getCurrentTrack()!!.artist.answer == Answer.CORRECT + || gameManager.getCurrentTrack()!!.title.answer == Answer.CORRECT && gameManager.getCurrentTrack()!!.artist.answer == Answer.CORRECT) { AsyncImage( - model = currentTrack!!.album, + model = gameManager.getCurrentTrack()!!.album, contentDescription = "Image from URL", modifier = Modifier .width(200.dp) @@ -91,7 +107,7 @@ fun GameScreen(navController: NavHostController) { ) } else { AsyncImage( - model = currentTrack!!.album, + model = gameManager.getCurrentTrack()!!.album, contentDescription = "Image from URL", modifier = Modifier .width(200.dp) @@ -103,14 +119,14 @@ fun GameScreen(navController: NavHostController) { } Row { - if (currentTrack!!.title.answer == Answer.CORRECT || currentTrack!!.title.answer == Answer.UNKNOWN) { - Text("Titre : ${currentTrack!!.title.value}", fontSize = 20.sp) + if (gameManager.getCurrentTrack()!!.title.answer == Answer.CORRECT || gameManager.getCurrentTrack()!!.title.answer == Answer.UNKNOWN) { + Text("Titre : ${gameManager.getCurrentTrack()!!.title.value}", fontSize = 20.sp) } else { TextField( value = titleInput.value, onValueChange = { titleInput.value = it; - gameManager.checkTitleAnswer(currentTrack, titleInput.value) + gameManager.checkTitleAnswer(gameManager.getCurrentTrack(), titleInput.value) }, label = { Text("Titre") }, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text), @@ -120,12 +136,12 @@ fun GameScreen(navController: NavHostController) { } Row { - if (currentTrack!!.artist.answer == Answer.CORRECT || currentTrack!!.artist.answer == Answer.UNKNOWN) { - Text("Artiste : ${currentTrack!!.artist.value}", fontSize = 20.sp) + if (gameManager.getCurrentTrack()!!.artist.answer == Answer.CORRECT || gameManager.getCurrentTrack()!!.artist.answer == Answer.UNKNOWN) { + Text("Artiste : ${gameManager.getCurrentTrack()!!.artist.value}", fontSize = 20.sp) } else { TextField( value = artistInput.value, - onValueChange = { artistInput.value = it; gameManager.checkArtistAnswer(currentTrack, artistInput.value) }, + onValueChange = { artistInput.value = it; gameManager.checkArtistAnswer(gameManager.getCurrentTrack(), artistInput.value) }, label = { Text("Artiste") }, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text), modifier = Modifier.fillMaxWidth() @@ -139,7 +155,6 @@ fun GameScreen(navController: NavHostController) { titleInput.value = "" artistInput.value = "" gameManager.nextTrack() - currentTrack = gameManager.getCurrentTrack() }, ) { Text(context.resources.getString(R.string.skip)) } diff --git a/app/src/main/java/fr/univpau/queezer/screen/HomeScreen.kt b/app/src/main/java/fr/univpau/queezer/screen/HomeScreen.kt index 86ab407..c9035df 100644 --- a/app/src/main/java/fr/univpau/queezer/screen/HomeScreen.kt +++ b/app/src/main/java/fr/univpau/queezer/screen/HomeScreen.kt @@ -1,5 +1,6 @@ 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.Spacer @@ -7,16 +8,20 @@ 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.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight 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.ui.theme.Purple40 @Composable @@ -28,15 +33,14 @@ fun HomeScreen(navController: NavHostController) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - // Logo et Titre -// Image( -// painter = painterResource(id = R.drawable.logo), -// contentDescription = "Logo Queezer", -// modifier = Modifier -// .size(120.dp) -// .padding(16.dp), -// contentScale = ContentScale.Crop -// ) + // Logo foreground et background + Image( + painter = painterResource(id = R.mipmap.ic_launcher), + contentDescription = "Logo Queezer", + modifier = Modifier.size(140.dp), + contentScale = ContentScale.Crop + ) + Text( text = "Queezer", fontSize = 32.sp, @@ -78,7 +82,7 @@ fun HomeScreen(navController: NavHostController) { } Button( - onClick = { /* TODO: Scores */ }, + onClick = { navController.navigate("score") }, shape = RoundedCornerShape(8.dp), modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/fr/univpau/queezer/screen/ScoreScreen.kt b/app/src/main/java/fr/univpau/queezer/screen/ScoreScreen.kt new file mode 100644 index 0000000..b05189d --- /dev/null +++ b/app/src/main/java/fr/univpau/queezer/screen/ScoreScreen.kt @@ -0,0 +1,164 @@ +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? = 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)) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/screen/SettingsScreen.kt b/app/src/main/java/fr/univpau/queezer/screen/SettingsScreen.kt index 4150653..3b9daba 100644 --- a/app/src/main/java/fr/univpau/queezer/screen/SettingsScreen.kt +++ b/app/src/main/java/fr/univpau/queezer/screen/SettingsScreen.kt @@ -27,7 +27,6 @@ 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) { diff --git a/app/src/main/java/fr/univpau/queezer/service/Converter.kt b/app/src/main/java/fr/univpau/queezer/service/Converter.kt new file mode 100644 index 0000000..6a23824 --- /dev/null +++ b/app/src/main/java/fr/univpau/queezer/service/Converter.kt @@ -0,0 +1,66 @@ +package fr.univpau.queezer.service + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import fr.univpau.queezer.data.Answer +import fr.univpau.queezer.data.Playlist +import fr.univpau.queezer.data.Settings +import fr.univpau.queezer.data.Track +import java.util.Date + +class Converters { + + private val gson = Gson() + + @TypeConverter + fun fromSettings(settings: Settings): String { + return gson.toJson(settings) + } + + @TypeConverter + fun toSettings(data: String): Settings { + return gson.fromJson(data, Settings::class.java) + } + + @TypeConverter + fun fromTrackList(tracks: List): String { + return gson.toJson(tracks) + } + + @TypeConverter + fun toTrackList(data: String): List { + val listType = object : TypeToken>() {}.type + return gson.fromJson(data, listType) + } + + @TypeConverter + fun fromAnswer(answer: Answer): String { + return answer.name + } + + @TypeConverter + fun toAnswer(data: String): Answer { + return Answer.valueOf(data) + } + + @TypeConverter + fun fromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + fun dateToTimestamp(date: Date?): Long? { + return date?.time + } + + @TypeConverter + fun fromPlaylist(playlist: Playlist): String { + return gson.toJson(playlist) + } + + @TypeConverter + fun toPlaylist(data: String): Playlist { + return gson.fromJson(data, Playlist::class.java) + } +} diff --git a/app/src/main/java/fr/univpau/queezer/service/DatabaseService.kt b/app/src/main/java/fr/univpau/queezer/service/DatabaseService.kt new file mode 100644 index 0000000..dfa59ef --- /dev/null +++ b/app/src/main/java/fr/univpau/queezer/service/DatabaseService.kt @@ -0,0 +1,34 @@ +package fr.univpau.queezer.service + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import fr.univpau.queezer.data.GameDao +import fr.univpau.queezer.data.Game + +@Database(entities = [Game::class], version = 2, exportSchema = false) +@TypeConverters(Converters::class) +abstract class DatabaseService : RoomDatabase() { + + abstract fun gameDao(): GameDao + + companion object { + @Volatile + private var INSTANCE: DatabaseService? = null + + fun getDatabase(context: Context): DatabaseService { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + DatabaseService::class.java, + "app_notes_database" + ).build() + INSTANCE = instance + + return instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/univpau/queezer/service/DeezerApiService.kt b/app/src/main/java/fr/univpau/queezer/service/DeezerApiService.kt index da7cc2b..9dd06af 100644 --- a/app/src/main/java/fr/univpau/queezer/service/DeezerApiService.kt +++ b/app/src/main/java/fr/univpau/queezer/service/DeezerApiService.kt @@ -6,6 +6,7 @@ import retrofit2.http.GET import retrofit2.http.Url data class PlaylistResponse( + val title: String, val tracks: TrackList ) diff --git a/app/src/main/java/fr/univpau/queezer/viewmodel/GameViewModel.kt b/app/src/main/java/fr/univpau/queezer/viewmodel/GameViewModel.kt new file mode 100644 index 0000000..9613ac0 --- /dev/null +++ b/app/src/main/java/fr/univpau/queezer/viewmodel/GameViewModel.kt @@ -0,0 +1,32 @@ +package fr.univpau.queezer.viewmodel + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fr.univpau.queezer.data.GameDao +import fr.univpau.queezer.data.Game +import kotlinx.coroutines.launch + +class GameViewModel(private val gameDao: GameDao) : ViewModel() { + + val games: LiveData> = gameDao.getAll() + + fun addGame(context: Context, game: Game) { + viewModelScope.launch { + try { + game.settings.validate(context) + gameDao.insert(game) + } catch (e: IllegalArgumentException) { + Log.e("GameViewModel", "Error adding game: ${e.message}") + } + } + } + + fun deleteGame(game: Game) { + viewModelScope.launch { + gameDao.delete(game) + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..9adc08c Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ce1bd0b Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..aa45079 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..1ed712a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..c51ae74 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..e32f6e0 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #D13DDC + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9cc33a..52d4d5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,4 +29,9 @@ Abandonner Valider + Nombre de parties jouées + Taux de réussite moyen + Filtrer par + Date + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92fc771..e6cb7ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,7 @@ [versions] agp = "8.7.3" +annotationsJava5 = "15.0" +coilCompose = "2.4.0" converterGson = "2.9.0" kotlin = "2.0.0" coreKtx = "1.13.1" @@ -10,12 +12,28 @@ lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.3" composeBom = "2024.04.01" navigationCompose = "2.8.4" -media3Exoplayer = "1.5.0" retrofit = "2.9.0" +roomCommon = "2.6.1" +roomCompiler = "2.6.1" +roomCompilerVersion = "2.5.0" +roomKtx = "2.6.1" +roomPaging = "2.6.1" +roomRuntime = "2.6.1" +roomRuntimeVersion = "2.5.0" +runtimeLivedata = "1.7.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" } +androidx-room-compiler-v250 = { module = "androidx.room:room-compiler", version.ref = "roomCompilerVersion" } +androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomPaging" } +androidx-room-room-compiler = { module = "androidx.room:room-compiler" } +androidx-room-room-compiler2 = { module = "androidx.room:room-compiler" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } +androidx-room-runtime-v250 = { module = "androidx.room:room-runtime", version.ref = "roomRuntimeVersion" } +annotations-java5 = { module = "org.jetbrains:annotations-java5", version.ref = "annotationsJava5" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -30,8 +48,12 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3Exoplayer" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "roomCommon" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } +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" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }