feat: Save locally all parties

This commit is contained in:
Lucàs
2025-01-12 19:23:06 +01:00
parent 2413ce55c9
commit 498517b2cb
41 changed files with 524 additions and 274 deletions
+3
View File
@@ -11,6 +11,9 @@
<SelectionState runConfigName="MainActivity">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="Queezer">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+19 -1
View File
@@ -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)
-1
View File
@@ -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">
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -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) }
}
}
@@ -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<Track>,
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()
)
@@ -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<List<Game>>
@Delete
fun delete(game: Game)
}
@@ -0,0 +1,6 @@
package fr.univpau.queezer.data
data class Playlist(
val title: String = "",
val tracks: List<Track> = emptyList()
)
@@ -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() }
}
}
@@ -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<Track> = emptyList()
// var tracks: List<Track> = emptyList()
var playlist: Playlist = Playlist()
var gameFinished : Boolean = false;
var currentTrackIndex: Int = 0;
var score = 0
constructor(settings: Settings, tracks: List<Track>) : 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)
}
}
}
@@ -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<Track> {
val deezerApiService = createDeezerApiService()
@@ -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)) }
@@ -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()
@@ -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<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))
}
}
}
@@ -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) {
@@ -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<Track>): String {
return gson.toJson(tracks)
}
@TypeConverter
fun toTrackList(data: String): List<Track> {
val listType = object : TypeToken<List<Track>>() {}.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)
}
}
@@ -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
}
}
}
}
@@ -6,6 +6,7 @@ import retrofit2.http.GET
import retrofit2.http.Url
data class PlaylistResponse(
val title: String,
val tracks: TrackList
)
@@ -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<List<Game>> = 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)
}
}
}
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#D13DDC</color>
</resources>
+5
View File
@@ -29,4 +29,9 @@
<string name="give_up">Abandonner</string>
<string name="submit">Valider</string>
<string name="games_played">Nombre de parties jouées</string>
<string name="average_success_rate">Taux de réussite moyen</string>
<string name="filters">Filtrer par</string>
<string name="date">Date</string>
</resources>
+24 -2
View File
@@ -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" }