diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/Game.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/Game.kt index 76fbf0f..67c1da4 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/Game.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/Game.kt @@ -2,6 +2,7 @@ package com.github.feelbeatapp.androidclient.game.datastreaming import com.github.feelbeatapp.androidclient.game.model.AudioState import com.github.feelbeatapp.androidclient.game.model.GameState +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness import com.github.feelbeatapp.androidclient.game.model.Player import com.github.feelbeatapp.androidclient.game.model.RoomStage import java.time.Duration @@ -39,4 +40,74 @@ class Game(private var gameState: GameState) { fun scheduleAudio(url: String, startAt: Instant, duration: Duration) { gameState = gameState.copy(audio = AudioState(url, startAt, duration = duration)) } + + fun markGuess(id: String) { + gameState = + gameState.copy( + songGuessMap = gameState.songGuessMap.plus(Pair(id, GuessCorrectness.VERIFYING)), + lastGuessStatus = GuessCorrectness.VERIFYING, + ) + } + + fun resolveGuess(songId: String, correct: Boolean) { + gameState = + gameState.copy( + songGuessMap = + gameState.songGuessMap.plus( + Pair( + songId, + if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT, + ) + ), + lastGuessStatus = + if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT, + ) + } + + fun setPlayerGuessResult(playerId: String, correct: Boolean) { + gameState = + gameState.copy( + playerGuessMap = + gameState.playerGuessMap.plus( + Pair( + playerId, + if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT, + ) + ) + ) + } + + fun addPoints(playerId: String, points: Int) { + gameState = + gameState.copy( + pointsMap = + gameState.pointsMap.plus( + Pair(playerId, gameState.pointsMap.getOrDefault(playerId, 0) + points) + ) + ) + } + + fun setCorrectSong(songId: String) { + gameState = + gameState.copy( + songGuessMap = gameState.songGuessMap.plus(Pair(songId, GuessCorrectness.CORRECT)) + ) + } + + fun resetGuessing() { + gameState = gameState.copy(songGuessMap = mapOf(), playerGuessMap = mapOf()) + } + + fun resetGame() { + gameState = + gameState.copy( + readyMap = mapOf(), + stage = RoomStage.LOBBY, + audio = null, + pointsMap = mapOf(), + songGuessMap = mapOf(), + playerGuessMap = mapOf(), + lastGuessStatus = GuessCorrectness.UNKNOWN, + ) + } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/GameDataStreamer.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/GameDataStreamer.kt index 8b140f1..64be086 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/GameDataStreamer.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/GameDataStreamer.kt @@ -1,6 +1,7 @@ package com.github.feelbeatapp.androidclient.game.datastreaming import com.github.feelbeatapp.androidclient.game.model.GameState +import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore import com.github.feelbeatapp.androidclient.game.model.RoomSettings import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.StateFlow @@ -12,7 +13,11 @@ interface GameDataStreamer { fun gameStateFlow(): StateFlow + fun lastGameResultStateFlow(): StateFlow> + suspend fun updateSettings(settings: RoomSettings) suspend fun sendReadyStatus(ready: Boolean) + + suspend fun sendGuess(id: String, points: Int) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/RemoteGameDataStreamer.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/RemoteGameDataStreamer.kt index 4cabb17..8082e54 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/RemoteGameDataStreamer.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/RemoteGameDataStreamer.kt @@ -1,19 +1,26 @@ package com.github.feelbeatapp.androidclient.game.datastreaming import android.util.Log +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.GuessSongMessage +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.GuessSongPayload import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.ReadyStatusMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.SettingsUpdateMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.SettingsUpdatePayload +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.CorrectSongMessage +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.EndGameMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialGameState import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.NewPlayerMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlaySongMessage +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerGuessMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerLeftMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerReadyMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.RoomStageMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerErrorMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerMessageType import com.github.feelbeatapp.androidclient.game.model.GameState +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness +import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore import com.github.feelbeatapp.androidclient.game.model.RoomSettings import com.github.feelbeatapp.androidclient.game.model.RoomStage import com.github.feelbeatapp.androidclient.infra.auth.AuthManager @@ -50,6 +57,7 @@ constructor( ) : GameDataStreamer { private var game: Game? = null private var gameStateFlow = MutableStateFlow(null) + private var resultStateFlow = MutableStateFlow>(listOf()) private var scope: CoroutineScope? = null override suspend fun joinRoom(roomId: String): Deferred = @@ -94,6 +102,10 @@ constructor( return gameStateFlow.asStateFlow() } + override fun lastGameResultStateFlow(): StateFlow> { + return resultStateFlow.asStateFlow() + } + override suspend fun updateSettings(settings: RoomSettings) { withContext(Dispatchers.IO) { val token = authManager.getAccessToken() @@ -112,16 +124,51 @@ constructor( return } + game it.setMyReadyStatus(ready) gameStateFlow.value = it.gameState() } } + resultStateFlow.value = listOf() + withContext(Dispatchers.IO) { networkClient.sendMessage(Json.encodeToString(ReadyStatusMessage(payload = ready))) } } + override suspend fun sendGuess(id: String, points: Int) { + synchronized(this) { + game.let { + if (it == null) { + return + } + if ( + it.gameState().songGuessMap.getOrDefault(id, GuessCorrectness.UNKNOWN) != + GuessCorrectness.UNKNOWN + ) { + return + } + + it.markGuess(id) + + if (points == 0) { + it.setPlayerGuessResult(it.gameState().me ?: "", false) + } + + gameStateFlow.value = it.gameState() + } + } + + withContext(Dispatchers.IO) { + networkClient.sendMessage( + Json.encodeToString( + GuessSongMessage(payload = GuessSongPayload(id = id, points = points)) + ) + ) + } + } + private suspend fun processMessage(content: String) { try { val type = Json.decodeFromString(content)["type"]?.jsonPrimitive?.content @@ -135,6 +182,9 @@ constructor( ServerMessageType.PLAYER_READY.name -> processPlayerReady(content) ServerMessageType.ROOM_STAGE.name -> processRoomStage(content) ServerMessageType.PLAY_SONG.name -> processPlaySong(content) + ServerMessageType.PLAYER_GUESS.name -> processPlayerGuess(content) + ServerMessageType.CORRECT_SONG.name -> processCorrectSong(content) + ServerMessageType.END_GAME.name -> processEndGame(content) else -> Log.w("RemoteGameDataStreamer", "Received unexpected message: $content") } } catch (e: Exception) { @@ -164,6 +214,10 @@ constructor( readyMap = initialState.readyMap, stage = RoomStage.LOBBY, audio = null, + pointsMap = initialState.players.associateBy({ it.id }, { 0 }), + songGuessMap = mapOf(), + playerGuessMap = mapOf(), + lastGuessStatus = GuessCorrectness.VERIFYING, ) ) gameStateFlow.value = game?.gameState() @@ -207,8 +261,62 @@ constructor( val start = Instant.ofEpochSecond(payload.timestamp) synchronized(this) { - game?.scheduleAudio(payload.url, start, Duration.ofMillis(payload.duration)) + game.let { + if (it == null) { + return + } + + it.scheduleAudio(payload.url, start, Duration.ofMillis(payload.duration)) + it.resetGuessing() + gameStateFlow.value = it.gameState() + } + } + } + + private fun processPlayerGuess(content: String) { + val payload = Json.decodeFromString(content).payload + + synchronized(this) { + game.let { + if (game == null) { + return + } + + if (payload.songId.isNotEmpty()) { + it?.resolveGuess(payload.songId, payload.correct) + } + + it?.setPlayerGuessResult(payload.playerId, payload.correct) + it?.addPoints(payload.playerId, payload.points) + + gameStateFlow.value = it?.gameState() + } + } + } + + private fun processCorrectSong(content: String) { + val correctSongId = Json.decodeFromString(content).payload + + synchronized(this) { + game?.setCorrectSong(correctSongId) gameStateFlow.value = game?.gameState() } } + + private fun processEndGame(content: String) { + val payload = Json.decodeFromString(content).payload + + synchronized(this) { + game.let { + if (it == null) { + return + } + + it.resetGame() + gameStateFlow.value = it.gameState() + } + } + + resultStateFlow.value = payload.results + } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ClientMessageType.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ClientMessageType.kt index 4af22a8..bcefc1d 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ClientMessageType.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ClientMessageType.kt @@ -3,4 +3,5 @@ package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client enum class ClientMessageType { SETTINGS_UPDATE, READY_STATUS, + GUESS_SONG, } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/GuessSongMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/GuessSongMessage.kt new file mode 100644 index 0000000..0032a49 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/GuessSongMessage.kt @@ -0,0 +1,12 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client + +import kotlinx.serialization.Required +import kotlinx.serialization.Serializable + +@Serializable +data class GuessSongMessage( + @Required val type: String = ClientMessageType.GUESS_SONG.name, + val payload: GuessSongPayload, +) + +@Serializable data class GuessSongPayload(val id: String, val points: Int) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/CorrectSongMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/CorrectSongMessage.kt new file mode 100644 index 0000000..877c11c --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/CorrectSongMessage.kt @@ -0,0 +1,9 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server + +import kotlinx.serialization.Serializable + +@Serializable +data class CorrectSongMessage( + override val type: String = ServerMessageType.CORRECT_SONG.name, + val payload: String, +) : ServerMessage() diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/EndGameMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/EndGameMessage.kt new file mode 100644 index 0000000..d4bc1cf --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/EndGameMessage.kt @@ -0,0 +1,14 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server + +import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("END_GAME") +data class EndGameMessage( + override val type: String = ServerMessageType.END_GAME.name, + val payload: EndGamePayload, +) : ServerMessage() + +@Serializable data class EndGamePayload(val results: List) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerGuessMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerGuessMessage.kt new file mode 100644 index 0000000..a743063 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerGuessMessage.kt @@ -0,0 +1,19 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("PLAYER_GUESS") +data class PlayerGuessMessage( + override val type: String = ServerMessageType.PLAYER_GUESS.name, + val payload: PlayerGuessPayload, +) : ServerMessage() + +@Serializable +data class PlayerGuessPayload( + val correct: Boolean, + val points: Int, + val playerId: String, + val songId: String, +) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerMessage.kt index ec5c73b..41430dd 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerMessage.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerMessage.kt @@ -10,6 +10,9 @@ enum class ServerMessageType { PLAYER_READY, ROOM_STAGE, PLAY_SONG, + PLAYER_GUESS, + CORRECT_SONG, + END_GAME, } @Serializable diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GameState.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GameState.kt index 0abe5da..948c14d 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GameState.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GameState.kt @@ -12,4 +12,8 @@ data class GameState( val readyMap: Map, val stage: RoomStage, val audio: AudioState?, + val pointsMap: Map, + val songGuessMap: Map, + val playerGuessMap: Map, + val lastGuessStatus: GuessCorrectness, ) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GuessCorrectness.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GuessCorrectness.kt new file mode 100644 index 0000000..9f75b5d --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GuessCorrectness.kt @@ -0,0 +1,8 @@ +package com.github.feelbeatapp.androidclient.game.model + +enum class GuessCorrectness { + UNKNOWN, + VERIFYING, + CORRECT, + INCORRECT, +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/Player.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/Player.kt index a9be423..f3880f0 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/Player.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/Player.kt @@ -2,4 +2,4 @@ package com.github.feelbeatapp.androidclient.game.model import kotlinx.serialization.Serializable -@Serializable data class Player(val id: String, val name: String, val imageUrl: String, val score: Int = 0) +@Serializable data class Player(val id: String, val name: String, val imageUrl: String) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/PlayerFinalScore.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/PlayerFinalScore.kt new file mode 100644 index 0000000..1c437ff --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/PlayerFinalScore.kt @@ -0,0 +1,9 @@ +package com.github.feelbeatapp.androidclient.game.model + +import kotlinx.serialization.Serializable + +@Serializable +data class PlayerFinalScore( + val profile: Player , + val points: Int +) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/audio/AudioController.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/audio/AudioController.kt index bd12c87..4d822a8 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/audio/AudioController.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/audio/AudioController.kt @@ -2,7 +2,9 @@ package com.github.feelbeatapp.androidclient.infra.audio import android.content.ComponentName import android.content.Context +import android.net.Uri import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.common.Player import androidx.media3.common.Player.Listener import androidx.media3.session.MediaController @@ -15,6 +17,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +const val FEELBEAT_LOGO_URL = + "https://github.com/FeelBeatApp/FeelBeatAndroidClient/blob/main/app/src/main/res/drawable/logo.svg" + data class PlaybackState(val isPlaying: Boolean = false, val progress: Long = 0) class AudioController @Inject constructor(@ApplicationContext ctx: Context) : Listener { @@ -81,10 +86,21 @@ class AudioController @Inject constructor(@ApplicationContext ctx: Context) : Li controllerFuture.addListener({ seek(to) }, MoreExecutors.directExecutor()) } else { controller.seekTo(to) + if (!controller.isPlaying) { + controller.play() + } } } private fun getMediaItemFromUri(uri: String): MediaItem { return MediaItem.fromUri(uri) + .buildUpon() + .setMediaMetadata( + MediaMetadata.Builder() + .setArtworkUri(Uri.parse(FEELBEAT_LOGO_URL)) + .setTitle("Guess the song") + .build() + ) + .build() } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppScreen.kt index 2aef7a5..19023c5 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppScreen.kt @@ -1,8 +1,6 @@ package com.github.feelbeatapp.androidclient.ui.app -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,6 +10,7 @@ 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.KeyboardArrowLeft +import androidx.compose.material3.Button import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -152,18 +151,17 @@ fun UserAccountBottomSheetContent(playerIdentity: PlayerIdentity?, onLogoutClick ) } Text(text = playerIdentity?.name ?: "...", style = MaterialTheme.typography.titleMedium) - Box( + Button( + onClick = { onLogoutClick() }, modifier = Modifier.fillMaxWidth() - .background(MaterialTheme.colorScheme.primary, MaterialTheme.shapes.medium) - .padding(8.dp), - contentAlignment = Alignment.Center, + .background(MaterialTheme.colorScheme.primary, MaterialTheme.shapes.medium), ) { Text( text = "log out", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.padding(vertical = 4.dp).clickable { onLogoutClick() }, + modifier = Modifier, ) } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/components/PlayerCard.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/components/PlayerCard.kt index 04d4700..d70cb9e 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/components/PlayerCard.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/components/PlayerCard.kt @@ -36,8 +36,8 @@ fun PlayerCard( Box(modifier = Modifier.size(size).clip(CircleShape)) { AsyncImage( model = player.imageUrl, - error = painterResource(R.drawable.userimage), - placeholder = painterResource(R.drawable.userimage), + error = painterResource(R.drawable.account), + placeholder = painterResource(R.drawable.account), contentDescription = stringResource(R.string.player_image), modifier = Modifier.size(size) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/GuessResult.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/GuessResult.kt deleted file mode 100644 index 4c353cc..0000000 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/GuessResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.feelbeatapp.androidclient.ui.app.game - -enum class GuessResult { - CORRECT, - INCORRECT, -} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/components/PlayerGameBadge.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/components/PlayerGameBadge.kt index bcf0908..1054f47 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/components/PlayerGameBadge.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/components/PlayerGameBadge.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.github.feelbeatapp.androidclient.R -import com.github.feelbeatapp.androidclient.ui.app.game.GuessResult +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme const val ICON_SIZE = .8f @@ -35,8 +35,8 @@ const val ICON_SIZE = .8f @Composable fun PlayerGameBadge( imageUrl: String, - points: Int, - result: GuessResult? = null, + points: Int? = null, + result: GuessCorrectness = GuessCorrectness.UNKNOWN, size: Dp = 80.dp, modifier: Modifier = Modifier, ) { @@ -44,8 +44,8 @@ fun PlayerGameBadge( Box(modifier = Modifier.size(size).clip(CircleShape)) { AsyncImage( model = imageUrl, - error = painterResource(R.drawable.userimage), - placeholder = painterResource(R.drawable.userimage), + error = painterResource(R.drawable.account), + placeholder = painterResource(R.drawable.account), contentDescription = stringResource(R.string.player_image), modifier = Modifier.size(size) @@ -55,9 +55,10 @@ fun PlayerGameBadge( val color = when (result) { - null -> Color.Transparent - GuessResult.CORRECT -> Color.Green.copy(alpha = .7f) - GuessResult.INCORRECT -> Color.Red.copy(alpha = .7f) + GuessCorrectness.CORRECT -> MaterialTheme.colorScheme.primary.copy(alpha = .7f) + GuessCorrectness.INCORRECT -> + MaterialTheme.colorScheme.errorContainer.copy(alpha = .7f) + else -> Color.Transparent } Column( @@ -65,23 +66,31 @@ fun PlayerGameBadge( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(color), ) { - if (result != null) { + if (result == GuessCorrectness.CORRECT) { Icon( - if (result == GuessResult.CORRECT) Icons.Default.Done - else Icons.Default.Close, + Icons.Default.Done, contentDescription = "correct", tint = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxSize(ICON_SIZE), ) + } else if (result == GuessCorrectness.INCORRECT) { + Icon( + Icons.Default.Close, + contentDescription = "incorrect", + tint = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxSize(ICON_SIZE), + ) } } } - Text( - text = points.toString(), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(top = 4.dp), - ) + if (points != null) { + Text( + text = points.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(top = 4.dp), + ) + } } } @@ -103,7 +112,7 @@ fun PlayerBadgeCorrectPreview() { PlayerGameBadge( imageUrl = "https://cdn-icons-png.flaticon.com/512/219/219983.png", points = 200, - result = GuessResult.CORRECT, + result = GuessCorrectness.CORRECT, ) } } @@ -115,7 +124,7 @@ fun PlayerBadgeIncorrectPreview() { PlayerGameBadge( imageUrl = "https://cdn-icons-png.flaticon.com/512/219/219983.png", points = 200, - result = GuessResult.INCORRECT, + result = GuessCorrectness.INCORRECT, ) } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultScreen.kt index a451831..320061c 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultScreen.kt @@ -1,67 +1,96 @@ package com.github.feelbeatapp.androidclient.ui.app.game.gameresult +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import coil3.compose.AsyncImage +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.game.model.Player +import com.github.feelbeatapp.androidclient.ui.app.navigation.AppRoute @Composable fun GameResultScreen( onNavigate: (String) -> Unit, viewModel: GameResultViewModel = hiltViewModel(), ) { - // val players by viewModel.players.collectAsState() - // - // Column( - // modifier = Modifier.fillMaxSize().padding(16.dp), - // verticalArrangement = Arrangement.SpaceBetween, - // horizontalAlignment = Alignment.CenterHorizontally, - // ) { - // Text( - // text = "Game Results", - // style = MaterialTheme.typography.headlineMedium, - // fontWeight = FontWeight.Bold, - // ) - // - // LazyColumn( - // modifier = Modifier.fillMaxWidth(), - // verticalArrangement = Arrangement.spacedBy(8.dp), - // contentPadding = PaddingValues(8.dp), - // ) { - // items(players) { player -> PlayerScoreItem(player = player) } - // } - // - // Button( - // onClick = { onNavigate(AppRoute.HOME.name) }, - // modifier = Modifier.padding(vertical = 16.dp), - // ) { - // Text(text = "CLOSE") - // } - // } - // } - // - // @Composable - // fun PlayerScoreItem(player: Player) { - // Box(modifier = Modifier.fillMaxWidth().padding(8.dp), contentAlignment = Alignment.Center) - // { - // Row(verticalAlignment = Alignment.CenterVertically, modifier = - // Modifier.fillMaxWidth()) { - // AsyncImage( - // model = player.imageUrl, - // placeholder = painterResource(R.drawable.userimage), - // error = painterResource(R.drawable.userimage), - // contentDescription = stringResource(R.string.player_avatar), - // modifier = Modifier.size(48.dp).clip(CircleShape), - // ) - // - // Text( - // text = "${player.name}: ${player.score} points", - // style = MaterialTheme.typography.bodyLarge, - // fontWeight = FontWeight.Bold, - // color = Color.Black, - // modifier = Modifier.padding(start = 16.dp), - // ) - // } - // } + val gameResultState by viewModel.gameResultState.collectAsState() + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Game Results", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(8.dp), + ) { + items(gameResultState.results.sortedByDescending { it.points }) { result -> + PlayerScoreItem(player = result.profile, score = result.points) + } + } + + Button( + onClick = { + onNavigate(AppRoute.ROOM_LOBBY.withArgs(mapOf("roomId" to gameResultState.roomId))) + }, + modifier = Modifier.padding(vertical = 16.dp), + ) { + Text(text = "CLOSE") + } + } +} + +@Composable +fun PlayerScoreItem(player: Player, score: Int) { + Box(modifier = Modifier.fillMaxWidth().padding(8.dp), contentAlignment = Alignment.Center) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + AsyncImage( + model = player.imageUrl, + placeholder = painterResource(R.drawable.account), + error = painterResource(R.drawable.account), + contentDescription = stringResource(R.string.player_avatar), + modifier = Modifier.size(48.dp).clip(CircleShape), + ) + + Text( + text = "${player.name}: $score points", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 16.dp), + ) + } + } } @Preview diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultViewModel.kt index e11640c..1199938 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/gameresult/GameResultViewModel.kt @@ -1,38 +1,42 @@ package com.github.feelbeatapp.androidclient.ui.app.game.gameresult -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.feelbeatapp.androidclient.game.datastreaming.GameDataStreamer -import com.github.feelbeatapp.androidclient.game.model.Player +import com.github.feelbeatapp.androidclient.game.model.GameState +import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +data class GameResultState(val roomId: String = "", val results: List = listOf()) + @HiltViewModel class GameResultViewModel @Inject constructor(private val gameDataStreamer: GameDataStreamer) : ViewModel() { - - private val _players = MutableStateFlow>(emptyList()) - val players: StateFlow> = _players + private val _gameResultState = MutableStateFlow(GameResultState()) + val gameResultState = _gameResultState.asStateFlow() init { - loadGameResults() - sortPlayers() - } + updateRoomId(gameDataStreamer.gameStateFlow().value) + viewModelScope.launch { gameDataStreamer.gameStateFlow().collect { updateRoomId(it) } } - private fun loadGameResults() { + _gameResultState.update { + it.copy(results = gameDataStreamer.lastGameResultStateFlow().value) + } viewModelScope.launch { - gameDataStreamer.gameStateFlow().collect { gameState -> - _players.value = gameState?.players ?: listOf() - Log.d("yup", "updating") + gameDataStreamer.lastGameResultStateFlow().collect { results -> + _gameResultState.update { it.copy(results = results) } } } } - private fun sortPlayers() { - _players.value = _players.value.sortedByDescending { player -> player.score } + private fun updateRoomId(gameState: GameState?) { + if (gameState != null) { + _gameResultState.update { it.copy(roomId = gameState.roomId) } + } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessResultScreen.kt index bd24ef6..fa27efa 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessResultScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessResultScreen.kt @@ -1,131 +1,135 @@ package com.github.feelbeatapp.androidclient.ui.app.game.guesssong 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.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness +import com.github.feelbeatapp.androidclient.ui.app.game.components.PlayerGameBadge +import com.github.feelbeatapp.androidclient.ui.app.game.guesssong.components.AudioPlayerControls -@Composable -fun GuessResultScreen( - roomId: String, - onNavigate: (String) -> Unit, - viewModel: GuessSongViewModel = hiltViewModel(), -) { - // val guessState by viewModel.guessState.collectAsState() - // - // Column( - // modifier = Modifier.fillMaxSize().padding(16.dp), - // horizontalAlignment = Alignment.CenterHorizontally, - // verticalArrangement = Arrangement.SpaceBetween, - // ) { - // Column( - // horizontalAlignment = Alignment.CenterHorizontally, - // modifier = Modifier.fillMaxWidth(), - // ) { - // Row( - // horizontalArrangement = Arrangement.SpaceEvenly, - // modifier = Modifier.fillMaxWidth(), - // ) { - // guessState.players.forEach { playerWithResult -> - // PlayerStatusIcon( - // image = playerWithResult.player.imageUrl, - // isCorrect = (playerWithResult.resultStatus == ResultStatus.CORRECT), - // ) - // } - // } - // - // guessState.currentSong?.let { song -> - // Box( - // modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth(), - // contentAlignment = Alignment.Center, - // ) { - // SongInfo(songTitle = song.title) - // } - // } - // } - // - // Text( - // text = - // if (guessState.players.any { it.resultStatus == ResultStatus.CORRECT }) { - // stringResource(R.string.you_guessed_song_correctly) - // } else { - // stringResource(R.string.ups_that_s_not_correct_answer) - // }, - // style = MaterialTheme.typography.headlineLarge, - // fontWeight = FontWeight.Bold, - // color = MaterialTheme.colorScheme.primary, - // modifier = Modifier.padding(top = 16.dp), - // ) - // - // Text( - // text = "Points: ${guessState.players.sumOf { it.points }}", - // style = MaterialTheme.typography.headlineMedium, - // fontWeight = FontWeight.Bold, - // modifier = Modifier.padding(bottom = 32.dp), - // ) - // - // Button(onClick = { onNavigate(AppRoute.GUESS.withArgs(mapOf("roomId" to roomId))) }) { - // Text(text = "NEXT") - // } - // } -} +const val CARD_WIDTH = .8f @Composable -fun PlayerStatusIcon(image: String, isCorrect: Boolean) { - Box(contentAlignment = Alignment.TopEnd) { - AsyncImage( - model = image, - placeholder = painterResource(R.drawable.userimage), - error = painterResource(R.drawable.userimage), - contentDescription = "Player Avatar", - modifier = Modifier.size(60.dp).clip(CircleShape), - ) - Text( - text = if (isCorrect) "✔" else "✖", - style = MaterialTheme.typography.bodySmall, - color = - if (isCorrect) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.error, - modifier = Modifier.align(Alignment.TopEnd).padding(4.dp), - ) +fun GuessResultScreen(viewModel: GuessSongViewModel = hiltViewModel()) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val playbackState by viewModel.playbackState.collectAsStateWithLifecycle() + + LaunchedEffect(null) { + viewModel.pause() + viewModel.play() } -} -@Composable -fun SongInfo(songTitle: String) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, ) { - Column { - Text( - text = songTitle, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth(), + ) { + uiState.players.forEach { playerWithResult -> + PlayerGameBadge( + imageUrl = playerWithResult.player.imageUrl, + points = uiState.pointsMap[playerWithResult.player.id] ?: 0, + size = 40.dp, + result = playerWithResult.status, + ) + } + } } + + val correctSong = uiState.songs.find { it.status == GuessCorrectness.CORRECT }?.song + if (correctSong != null) { + ElevatedCard( + modifier = Modifier.fillMaxWidth().heightIn(0.dp, 500.dp).weight(1f).padding(24.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(CARD_WIDTH).heightIn(0.dp, 300.dp), + ) { + AsyncImage( + model = correctSong.imageUrl, + contentDescription = "song", + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize(), + ) + } + Text( + text = correctSong.title, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineLarge, + ) + + Text( + text = correctSong.artist, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineSmall, + ) + } + } + } + + Text( + text = + if (uiState.lastGuessCorrect) { + stringResource(R.string.you_guessed_song_correctly) + } else { + stringResource(R.string.ups_that_s_not_correct_answer) + }, + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(top = 16.dp), + ) + + HorizontalDivider() + + AudioPlayerControls( + value = playbackState.progress, + onValueChange = { viewModel.seek(it) }, + duration = uiState.songDuration, + isPlaying = playbackState.isPlaying, + onPlayPause = { isPlaying -> if (isPlaying) viewModel.pause() else viewModel.play() }, + ) } } @Preview @Composable fun GuessResultScreenPreview() { - GuessResultScreen("roomid", {}) + GuessResultScreen() } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongScreen.kt index a6750bf..f5a9de0 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongScreen.kt @@ -1,35 +1,49 @@ package com.github.feelbeatapp.androidclient.ui.app.game.guesssong import androidx.annotation.OptIn -import androidx.compose.foundation.border +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background 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.Spacer 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.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.util.UnstableApi import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness import com.github.feelbeatapp.androidclient.game.model.Song import com.github.feelbeatapp.androidclient.ui.app.components.SongCard import com.github.feelbeatapp.androidclient.ui.app.game.components.PlayerGameBadge @@ -37,11 +51,7 @@ import com.github.feelbeatapp.androidclient.ui.app.game.guesssong.components.Aud @OptIn(UnstableApi::class) @Composable -fun GuessSongScreen( - roomId: String, - onNavigate: (String) -> Unit, - viewModel: GuessSongViewModel = hiltViewModel(), -) { +fun GuessSongScreen(viewModel: GuessSongViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val playbackState by viewModel.playbackState.collectAsStateWithLifecycle() @@ -52,12 +62,39 @@ fun GuessSongScreen( verticalArrangement = Arrangement.spacedBy(8.dp), ) { Row( - horizontalArrangement = Arrangement.SpaceEvenly, + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), ) { - uiState.players.forEach { player -> - PlayerGameBadge(imageUrl = player.imageUrl, points = 200, size = 40.dp) + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().weight(1f), + ) { + items(uiState.players) { p -> + PlayerGameBadge(imageUrl = p.player.imageUrl, size = 40.dp, result = p.status) + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + VerticalDivider(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text( + uiState.cumulatedPoints.toString(), + style = MaterialTheme.typography.titleSmall, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("+", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.width(4.dp)) + Text( + uiState.pointsToWin.toString(), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("points") } } @@ -72,13 +109,19 @@ fun GuessSongScreen( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize().weight(1f), ) { - items(uiState.songs) { song -> SongItem(song = song, onClick = {}) } + items(uiState.songs.filter { it !in uiState.blacklisted }) { s -> + SongItem( + song = s.song, + onClick = { viewModel.guess(s.song.id) }, + correctness = s.status, + penalty = uiState.pointsPenalty, + removeFromList = { viewModel.blackList(s) }, + ) + } } HorizontalDivider() - Text(text = "${uiState.songDuration} ${playbackState.progress}") - AudioPlayerControls( value = playbackState.progress, onValueChange = { viewModel.seek(it) }, @@ -91,38 +134,72 @@ fun GuessSongScreen( @Composable fun SearchBar(searchQuery: String, onSearchQueryChange: (String) -> Unit) { - Row( - modifier = - Modifier.fillMaxWidth() - .border(1.dp, Color.Gray, MaterialTheme.shapes.small) - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - BasicTextField( + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + TextField( + trailingIcon = { + Icon(Icons.Default.Search, contentDescription = stringResource(R.string.search)) + }, value = searchQuery, onValueChange = onSearchQueryChange, singleLine = true, modifier = Modifier.weight(1f), ) - Icon(Icons.Default.Search, contentDescription = stringResource(R.string.search)) } } @Composable -fun SongItem(song: Song, onClick: () -> Unit) { - SongCard( - title = song.title, - artist = song.artist, - imageUrl = song.imageUrl, - duration = song.duration, - onClick = onClick, - size = 50.dp, - displayDuration = false, - ) +fun SongItem( + song: Song, + onClick: () -> Unit, + correctness: GuessCorrectness, + penalty: Int, + removeFromList: () -> Unit, +) { + val fade by + animateFloatAsState( + if (correctness == GuessCorrectness.INCORRECT) 1f else 0f, + animationSpec = tween(durationMillis = 300), + ) + + LaunchedEffect(fade) { + if (fade == 1f) { + removeFromList() + } + } + + Box(modifier = Modifier.height(50.dp)) { + SongCard( + title = song.title, + artist = song.artist, + imageUrl = song.imageUrl, + duration = song.duration, + onClick = onClick, + size = 50.dp, + displayDuration = false, + ) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.alpha(fade) + .fillMaxSize() + .clipToBounds() + .clip(CardDefaults.elevatedShape) + .background(MaterialTheme.colorScheme.errorContainer), + ) { + Text( + "- $penalty points", + style = MaterialTheme.typography.displaySmall, + color = Color.White, + fontWeight = FontWeight.Bold, + ) + } + } } @Preview @Composable fun GuessSongPreview() { - GuessSongScreen("roomId", {}) + GuessSongScreen() } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongViewModel.kt index e1ffac3..b47d390 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/GuessSongViewModel.kt @@ -4,24 +4,38 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.feelbeatapp.androidclient.game.datastreaming.GameDataStreamer import com.github.feelbeatapp.androidclient.game.model.GameState +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness import com.github.feelbeatapp.androidclient.game.model.Player import com.github.feelbeatapp.androidclient.game.model.Song import com.github.feelbeatapp.androidclient.infra.audio.AudioController import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.xdrop.fuzzywuzzy.FuzzySearch +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds data class GuessSongState( - val players: List = listOf(), + val players: List = listOf(), val query: String = "", - val songs: List = listOf(), + val songs: List = listOf(), + val blacklisted: List = listOf(), val songDuration: Long = 0, + val pointsToWin: Int = 0, + val cumulatedPoints: Int = 0, + val pointsMap: Map = mapOf(), + val lastGuessCorrect: Boolean = false, + val pointsPenalty: Int = 0, ) +data class SongState(val song: Song, val status: GuessCorrectness) + +data class PlayerState(val player: Player, val status: GuessCorrectness) + @HiltViewModel class GuessSongViewModel @Inject @@ -41,27 +55,63 @@ constructor( private fun updateState(gameState: GameState?) { _uiState.update { it.copy( - players = gameState?.players ?: listOf(), - songs = gameState?.songs ?: listOf(), + players = + gameState?.players?.map { player -> + PlayerState( + player, + gameState.playerGuessMap[player.id] ?: GuessCorrectness.UNKNOWN, + ) + } ?: listOf(), + songs = + gameState?.songs?.map { song -> + SongState(song, gameState.songGuessMap[song.id] ?: GuessCorrectness.UNKNOWN) + } ?: listOf(), songDuration = gameState?.audio?.duration?.toMillis() ?: 0, + cumulatedPoints = gameState?.pointsMap?.get(gameState.me) ?: 0, + pointsMap = gameState?.pointsMap ?: mapOf(), + lastGuessCorrect = gameState?.lastGuessStatus == GuessCorrectness.CORRECT, + pointsPenalty = gameState?.settings?.incorrectGuessPenalty ?: 0, ) } } - private fun fuzzySort(query: String): List { + private fun fuzzySort(query: String): List { return if (query.isBlank()) _uiState.value.songs else _uiState.value.songs.sortedByDescending { - FuzzySearch.ratio(it.title.lowercase(), query.lowercase()) + FuzzySearch.ratio(it.song.title.lowercase(), query.lowercase()) } } + fun blackList(song: SongState) { + _uiState.update { it.copy(blacklisted = it.blacklisted.plus(song)) } + } + fun updateSearchQuery(query: String) { _uiState.update { it.copy(query = query, songs = fuzzySort(query)) } } fun play() { audioController.play() + + _uiState.update { + it.copy(pointsToWin = gameDataStreamer.gameStateFlow().value?.settings?.basePoints ?: 0) + } + + viewModelScope.launch(Dispatchers.Default) { + while (true) { + delay(1.seconds) + val minus = + gameDataStreamer.gameStateFlow().value?.settings?.timePenaltyPerSecond ?: 0 + _uiState.update { it.copy(pointsToWin = it.pointsToWin - minus) } + + if (_uiState.value.pointsToWin <= 0) { + _uiState.update { it.copy(pointsToWin = 0) } + gameDataStreamer.sendGuess("", 0) + return@launch + } + } + } } fun pause() { @@ -71,4 +121,8 @@ constructor( fun seek(to: Long) { audioController.seek(to) } + + fun guess(songId: String) { + viewModelScope.launch { gameDataStreamer.sendGuess(songId, uiState.value.pointsToWin) } + } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/components/AudioPlayerControls.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/components/AudioPlayerControls.kt index 2e814ba..9f02ed1 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/components/AudioPlayerControls.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/guesssong/components/AudioPlayerControls.kt @@ -1,94 +1,30 @@ package com.github.feelbeatapp.androidclient.ui.app.game.guesssong.components -import android.util.Log import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animate import androidx.compose.animation.core.tween -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.height -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Slider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.media3.common.util.UnstableApi import com.github.feelbeatapp.androidclient.R -import com.github.feelbeatapp.androidclient.ui.app.uimodel.Song import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme -@androidx.annotation.OptIn(UnstableApi::class) -@SuppressWarnings("UnusedParameter", "MagicNumber", "MaxLineLength") -@Composable -fun MusicPlayer(song: Song?, viewModel: MusicPlayerViewModel = viewModel()) { - val songUrl = - "https://rr1---sn-f5f7kn7z.googlevideo.com/videoplayback?expire=1736640305&ei=0bKCZ8qTKaGP6dsPtp_tmQs&ip=89.64.59.248&id=o-ALE7qEciMqUy10n2Udgb3J3jnuA4vbFMQG1GLtYKkA_M&itag=251&source=youtube&requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&met=1736618705%2C&mh=7c&mm=31%2C26&mn=sn-f5f7kn7z%2Csn-4g5ednsl&ms=au%2Conr&mv=m&mvi=1&pl=13&rms=au%2Cau&initcwndbps=3535000&bui=AY2Et-MNwKqMAzX6OxUipsXZOxyIPdH-V6oiuntYe7nv6xnqaNEUPyvy8tC4_H5BtCBOdLJ6CKlggE3S&vprv=1&svpuc=1&mime=audio%2Fwebm&ns=FSxCnq53JRL7JoeBUsNPOKwQ&rqh=1&gir=yes&clen=3437753&dur=212.061&lmt=1717047822556748&mt=1736618225&fvip=3&keepalive=yes&fexp=51326932%2C51331020%2C51335594%2C51353498%2C51371294&c=MWEB&sefc=1&txp=4532434&n=Ll0zIouNSqK6tw&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&lsparams=met%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Crms%2Cinitcwndbps&lsig=AGluJ3MwRAIgG_3P45XFb2ga0dbUNLab75Si0KHVpqnzK8DrNvdqgZoCIGLHpIxBH9nZc0J71nV4gkDK7-L7MX3R9hNEaillUJXM&sig=AJfQdSswRgIhAPfijY_GFlfmFZMmZWCovzFhnP8Kpgmdn0RkpbnW14HlAiEAw3-fOda0dA3auJyS7pWBeMlrppg_LjBZRISI-R1pe6w%3D" - - LaunchedEffect(songUrl) { viewModel.loadSong(songUrl) } - - val currentPosition by viewModel.currentPosition.collectAsState() - val isPlaying by viewModel.isPlaying.collectAsState() - - val validDuration = viewModel.duration - val validPosition = currentPosition.coerceIn(0f, validDuration) - - Column( - modifier = Modifier.fillMaxWidth().padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Slider( - value = validPosition, - onValueChange = { newValue -> viewModel.seekTo(newValue) }, - valueRange = 0f..validDuration, - ) - - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { - Button(onClick = { viewModel.seekTo(maxOf(currentPosition - 5000f, 0f)) }) { - Text(text = "<<") - } - - Button( - onClick = { - if (isPlaying) { - viewModel.pause() - } else { - viewModel.play() - } - } - ) { - Text(text = if (isPlaying) "||" else "▶") - } - - Button(onClick = { viewModel.seekTo(minOf(currentPosition + 5000f, validDuration)) }) { - Text(text = ">>") - } - } - - Log.d("MusicPlayer", "Position: $currentPosition, Duration: $validDuration") - } -} - @Composable fun AudioPlayerControls( value: Long, @@ -127,7 +63,6 @@ fun AudioPlayerControls( if (isPlaying) { Icon( ImageVector.vectorResource(R.drawable.pause), - tint = Color.Black, contentDescription = "play/pause", ) } else { diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameScreen.kt index 055f704..3600a3a 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameScreen.kt @@ -42,10 +42,7 @@ fun StartGameScreen( Column( modifier = - Modifier - .fillMaxSize() - .padding(16.dp) - .background(MaterialTheme.colorScheme.background), + Modifier.fillMaxSize().padding(16.dp).background(MaterialTheme.colorScheme.background), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly, ) { @@ -56,8 +53,6 @@ fun StartGameScreen( if (startGameState.loading) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { CircularProgressIndicator( - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 4.dp, modifier = Modifier.width(50.dp).height(50.dp), ) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameViewModel.kt index 3d7d28f..ca6dda8 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/startgame/StartGameViewModel.kt @@ -18,10 +18,11 @@ import kotlinx.coroutines.launch data class StartGameState( val loading: Boolean = true, val players: List = listOf(), - val counter: Int = 5, + val counter: Int = DEFAULT_COUNTER, ) -const val SECOND_MS: Long = 1000 +const val DELAY: Long = 500 +const val DEFAULT_COUNTER = 5 @HiltViewModel class StartGameViewModel @@ -44,7 +45,7 @@ constructor( if (!counterLaunched) { viewModelScope.launch { while (startGameState.value.counter > 0) { - delay(SECOND_MS) + delay(DELAY) updateState(gameDataStreamer.gameStateFlow().value) } } @@ -54,14 +55,16 @@ constructor( } private fun updateState(gameState: GameState?) { + val counter = + if (gameState?.audio == null) DEFAULT_COUNTER + else if (Instant.now().isAfter(gameState.audio.startAt)) 0 + else Duration.between(Instant.now(), gameState.audio.startAt).seconds.toInt() + _startGameState.value = StartGameState( players = gameState?.players ?: listOf(), loading = gameState?.audio == null, - counter = - if (gameState?.audio != null) - Duration.between(Instant.now(), gameState.audio.startAt).seconds.toInt() - else 5, + counter = counter, ) if (gameState?.audio != null) { diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/home/HomeScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/home/HomeScreen.kt index 8b869cf..b1b3f99 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/home/HomeScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/home/HomeScreen.kt @@ -4,14 +4,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -55,27 +53,13 @@ fun HomeScreen( modifier = Modifier.padding(16.dp), ) - if (loading) { - Box( - modifier = Modifier.fillMaxWidth().padding(top = 16.dp), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - strokeWidth = 4.dp, - modifier = Modifier.size(50.dp), - ) - } - } else { - RoomList( - items = rooms, - isRefreshing = loading, - onRefresh = { homeViewModel.loadRooms() }, - onRoomSelect = onRoomSelect, - modifier = Modifier.fillMaxSize().padding(bottom = 80.dp), - ) - } + RoomList( + items = rooms, + isRefreshing = loading, + onRefresh = { homeViewModel.loadRooms() }, + onRoomSelect = onRoomSelect, + modifier = Modifier.fillMaxSize().padding(bottom = 80.dp), + ) } IconButton( diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeScreen.kt index 361fab8..2af9daf 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeScreen.kt @@ -59,8 +59,8 @@ fun LobbyHomeScreen(viewModel: LobbyHomeViewModel = hiltViewModel()) { model = lobbyState.playlistImageUrl, contentDescription = stringResource(R.string.playlist_image), contentScale = ContentScale.Fit, - placeholder = painterResource(R.drawable.userimage), - error = painterResource(R.drawable.userimage), + placeholder = painterResource(R.drawable.account), + error = painterResource(R.drawable.account), modifier = Modifier.fillMaxSize(), ) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/AppGraph.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/AppGraph.kt index 9659266..9e06f82 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/AppGraph.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/AppGraph.kt @@ -50,6 +50,9 @@ fun AppGraph( val lobbyState by lobbyViewModel.lobbyState.collectAsStateWithLifecycle() val stage by navigationViewModel.stage.collectAsStateWithLifecycle(RoomStage.LOBBY) val loadedRoomId by navigationViewModel.roomId.collectAsStateWithLifecycle(null) + val correctReceived by navigationViewModel.correctReceived.collectAsStateWithLifecycle(false) + val scheduledAudio by navigationViewModel.scheduledAudio.collectAsStateWithLifecycle(null) + val lastGameResult by navigationViewModel.lastGameResult.collectAsStateWithLifecycle() LaunchedEffect(lobbyState.joinFailed) { if (lobbyState.joinFailed) { @@ -68,6 +71,36 @@ fun AppGraph( } } + LaunchedEffect(correctReceived) { + if (correctReceived) { + navController.navigate( + AppRoute.GUESS_RESULT.withArgs(mapOf("roomId" to (loadedRoomId ?: ""))) + ) { + popUpTo(AppRoute.HOME.route) + } + } + } + + LaunchedEffect(scheduledAudio) { + if (scheduledAudio != null) { + navController.navigate( + AppRoute.START_GAME.withArgs(mapOf("roomId" to (loadedRoomId ?: ""))) + ) { + popUpTo(AppRoute.HOME.route) + } + } + } + + LaunchedEffect(lastGameResult) { + if (lastGameResult.isNotEmpty()) { + navController.navigate( + AppRoute.GAME_RESULT.withArgs(mapOf("roomId" to (loadedRoomId ?: ""))) + ) { + popUpTo(AppRoute.HOME.route) + } + } + } + var showExitDialog by remember { mutableStateOf(false) } ExitDialog( diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/GameGraph.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/GameGraph.kt index bda7449..c691c18 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/GameGraph.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/GameGraph.kt @@ -12,13 +12,9 @@ fun NavGraphBuilder.gameGraph(onNavigate: (String) -> Unit) { StartGameScreen(roomId = it.getRoomId()!!, onNavigate = onNavigate) } - composable(route = AppRoute.GUESS.route) { - GuessSongScreen(roomId = it.getRoomId()!!, onNavigate = onNavigate) - } + composable(route = AppRoute.GUESS.route) { GuessSongScreen() } - composable(route = AppRoute.GUESS_RESULT.route) { - GuessResultScreen(roomId = it.getRoomId()!!, onNavigate = onNavigate) - } + composable(route = AppRoute.GUESS_RESULT.route) { GuessResultScreen() } composable(route = AppRoute.GAME_RESULT.route) { GameResultScreen(onNavigate = onNavigate) } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/NavigationViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/NavigationViewModel.kt index 36ab137..4c4e1bd 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/NavigationViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/NavigationViewModel.kt @@ -2,13 +2,22 @@ package com.github.feelbeatapp.androidclient.ui.app.navigation import androidx.lifecycle.ViewModel import com.github.feelbeatapp.androidclient.game.datastreaming.GameDataStreamer +import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.map +import javax.inject.Inject @HiltViewModel class NavigationViewModel @Inject constructor(private val gameDataStreamer: GameDataStreamer) : ViewModel() { val stage = gameDataStreamer.gameStateFlow().map { it?.stage } val roomId = gameDataStreamer.gameStateFlow().map { it?.roomId } + val correctReceived = + gameDataStreamer.gameStateFlow().map { + it?.songGuessMap?.any { s -> s.value == GuessCorrectness.CORRECT } ?: false + } + + val scheduledAudio = gameDataStreamer.gameStateFlow().map { it?.audio } + + val lastGameResult = gameDataStreamer.lastGameResultStateFlow() } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/LobbyRoomSettingsScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/LobbyRoomSettingsScreen.kt index f186d6e..8f68882 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/LobbyRoomSettingsScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/LobbyRoomSettingsScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,8 +42,6 @@ fun LobbyRoomSettingsScreen( if (!editRoomState.loaded) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { CircularProgressIndicator( - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 4.dp, modifier = Modifier.width(50.dp).height(50.dp), ) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/NewRoomSettingsScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/NewRoomSettingsScreen.kt index 572e103..91429b6 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/NewRoomSettingsScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/screens/NewRoomSettingsScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -52,8 +51,6 @@ fun NewRoomSettingsScreen( if (loading) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { CircularProgressIndicator( - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 4.dp, modifier = Modifier.width(50.dp).height(50.dp), ) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/loading/LoadingScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/loading/LoadingScreen.kt index 38134db..457c4d5 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/loading/LoadingScreen.kt @@ -1,5 +1,6 @@ package com.github.feelbeatapp.androidclient.ui.loading +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -21,34 +22,30 @@ const val SPINNER_HEIGHT_OFFSET = 0.5f @Composable fun LoadingScreen(text: String = "Loading") { - FeelBeatTheme { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), + ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Bottom, + modifier = Modifier.fillMaxHeight(SPINNER_HEIGHT_OFFSET).padding(30.dp), ) { - Column( - verticalArrangement = Arrangement.Bottom, - modifier = Modifier.fillMaxHeight(SPINNER_HEIGHT_OFFSET).padding(30.dp), - ) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - strokeWidth = 8.dp, - modifier = Modifier.width(100.dp).height(100.dp), - ) - } - - Text( - text = text, - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.titleLarge, + CircularProgressIndicator( + strokeWidth = 8.dp, + modifier = Modifier.width(100.dp).height(100.dp), ) } + + Text( + text = text, + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleLarge, + ) } } @Preview(showBackground = true) @Composable fun LoadingScreenPreview() { - LoadingScreen() + FeelBeatTheme { LoadingScreen() } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/AuthActivity.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/AuthActivity.kt index 060687c..56d3768 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/AuthActivity.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/AuthActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.enableEdgeToEdge import com.github.feelbeatapp.androidclient.infra.auth.AuthManager import com.github.feelbeatapp.androidclient.ui.MainActivity import com.github.feelbeatapp.androidclient.ui.loading.LoadingScreen +import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,6 +44,6 @@ class AuthActivity : ComponentActivity() { finish() } - setContent { LoadingScreen() } + setContent { FeelBeatTheme { LoadingScreen() } } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/LoginScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/LoginScreen.kt index becf38c..ca613e7 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/LoginScreen.kt @@ -1,17 +1,27 @@ package com.github.feelbeatapp.androidclient.ui.login +import androidx.compose.foundation.Image +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.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable fun LoginScreen(loginViewModel: LoginViewModel = hiltViewModel(), modifier: Modifier = Modifier) { @@ -20,11 +30,40 @@ fun LoginScreen(loginViewModel: LoginViewModel = hiltViewModel(), modifier: Modi Column( verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), ) { - Text(stringResource(R.string.login_screen)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + painter = painterResource(R.drawable.logo), + contentDescription = "logo", + modifier = Modifier.width(600.dp), + ) + + Row { + Text( + "Feel", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.displayLarge, + ) + Text( + "Beat", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.displayLarge, + ) + } + } Button(onClick = { loginViewModel.login(ctx) }) { - Text(stringResource(R.string.login_with_spotify)) + Text( + stringResource(R.string.login_with_spotify), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(16.dp, 8.dp), + ) } } } + +@Composable +@Preview +fun LoginScreenPreview() { + FeelBeatTheme { LoginScreen() } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/theme/Theme.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/theme/Theme.kt index 63cc820..63d0708 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/theme/Theme.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/theme/Theme.kt @@ -1,49 +1,42 @@ package com.github.feelbeatapp.androidclient.ui.theme -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.Color private val DarkColorScheme = - darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80) + darkColorScheme( + primary = Color(0xFF1DB954), + onPrimary = Color.White, + background = Color(0xFF222222), + primaryContainer = Color(0xFF111111), + surface = Color(0xFF222222), + onSurface = Color.White, + onSurfaceVariant = Color(0xFFDDDDDD), + secondaryContainer = Color(0xFF111111), + onSecondaryContainer = Color(0xFF1DB954), + surfaceContainerHigh = Color(0xFF111111), + surfaceContainerHighest = Color(0xFF111111), + surfaceContainerLowest = Color(0xFF111111), + surfaceContainerLow = Color(0xFF111111), + onPrimaryContainer = Color(0xFFEEEEEE), + errorContainer = Color(0xFF93000A), + ) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + primary = Color(0xFF1DB954), + primaryContainer = Color(0xFFD3F8E0), + secondaryContainer = Color(0xFFE8E8E8), ) @Composable -fun FeelBeatTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit, -) { +fun FeelBeatTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme else -> LightColorScheme } diff --git a/app/src/main/res/drawable/logo.xml b/app/src/main/res/drawable/logo.xml new file mode 100644 index 0000000..6d0e4a6 --- /dev/null +++ b/app/src/main/res/drawable/logo.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/drawable/userimage.webp b/app/src/main/res/drawable/userimage.webp deleted file mode 100644 index 10641c5..0000000 Binary files a/app/src/main/res/drawable/userimage.webp and /dev/null differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d7b4b0d..5112789 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,7 +45,7 @@ Couldn\'t join the room Can\'t set max players, too much players in room Apply - WAITING FOR %1$s/%2$s + Waiting for %1$s/%2$s Exit room Are you sure you want to leave the room? It may prevent you from finishing the game yes