From 077d36d26b65b2fb60d8fae460e995b259e59427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Mon, 13 Jan 2025 15:20:40 +0100 Subject: [PATCH 1/3] tmp --- .../androidclient/game/datastreaming/Game.kt | 44 ++++++ .../game/datastreaming/GameDataStreamer.kt | 2 + .../datastreaming/RemoteGameDataStreamer.kt | 48 +++++++ .../messages/client/ClientMessageType.kt | 1 + .../messages/client/GuessSongMessage.kt | 12 ++ .../messages/server/PlayerGuessMessage.kt | 19 +++ .../messages/server/ServerMessage.kt | 1 + .../androidclient/game/model/GameState.kt | 3 + .../game/model/GuessCorrectness.kt | 7 + .../androidclient/game/model/Player.kt | 2 +- .../infra/audio/AudioController.kt | 5 +- .../androidclient/infra/di/AppModule.kt | 2 +- .../ui/app/game/components/PlayerGameBadge.kt | 16 ++- .../app/game/gameresult/GameResultScreen.kt | 133 +++++++++++------- .../game/gameresult/GameResultViewModel.kt | 2 +- .../ui/app/game/guesssong/GuessSongScreen.kt | 58 ++++++-- .../app/game/guesssong/GuessSongViewModel.kt | 19 +++ .../components/AudioPlayerControls.kt | 63 --------- .../app/game/startgame/StartGameViewModel.kt | 17 ++- .../androidclient/ui/app/home/HomeScreen.kt | 30 +--- .../ui/app/navigation/GameGraph.kt | 4 +- 21 files changed, 317 insertions(+), 171 deletions(-) create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/GuessSongMessage.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerGuessMessage.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GuessCorrectness.kt 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..cf26112 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,47 @@ 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)) + ) + } + + fun resolveGuess(songId: String, correct: Boolean) { + gameState = + gameState.copy( + playerGuessMap = + gameState.songGuessMap.plus( + Pair( + songId, + 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) + ) + ) + } } 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..894322f 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 @@ -15,4 +15,6 @@ interface GameDataStreamer { 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..8dfd097 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,6 +1,8 @@ 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 @@ -8,6 +10,7 @@ import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.I 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 @@ -122,6 +125,27 @@ constructor( } } + override suspend fun sendGuess(id: String, points: Int) { + synchronized(this) { + game.let { + if (it == null) { + return + } + + it.markGuess(id) + 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 +159,7 @@ 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) else -> Log.w("RemoteGameDataStreamer", "Received unexpected message: $content") } } catch (e: Exception) { @@ -164,6 +189,9 @@ constructor( readyMap = initialState.readyMap, stage = RoomStage.LOBBY, audio = null, + pointsMap = initialState.players.associateBy({ it.id }, { 0 }), + songGuessMap = mapOf(), + playerGuessMap = mapOf(), ) ) gameStateFlow.value = game?.gameState() @@ -211,4 +239,24 @@ constructor( gameStateFlow.value = game?.gameState() } } + + private fun processPlayerGuess(content: String) { + val payload = Json.decodeFromString(content).payload + + Log.d("yupi", "yay") + synchronized(this) { + game.let { + if (game == null) { + return + } + + if (payload.songId.isNotEmpty()) { + game?.resolveGuess(payload.songId, payload.correct) + } + + game?.setPlayerGuessResult(payload.playerId, payload.correct) + game?.addPoints(payload.playerId, payload.points) + } + } + } } 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/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..a7a9831 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,7 @@ enum class ServerMessageType { PLAYER_READY, ROOM_STAGE, PLAY_SONG, + PLAYER_GUESS } @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..0b625fc 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,7 @@ data class GameState( val readyMap: Map, val stage: RoomStage, val audio: AudioState?, + val pointsMap: Map, + val songGuessMap: Map, + val playerGuessMap: Map, ) 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..c150fd1 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/GuessCorrectness.kt @@ -0,0 +1,7 @@ +package com.github.feelbeatapp.androidclient.game.model + +enum class GuessCorrectness { + CORRECT, + INCORRECT, + VERIFYING, +} 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/infra/audio/AudioController.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/audio/AudioController.kt index bd12c87..ee876db 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 @@ -10,10 +10,10 @@ import androidx.media3.session.SessionToken import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import javax.inject.Inject data class PlaybackState(val isPlaying: Boolean = false, val progress: Long = 0) @@ -81,6 +81,9 @@ class AudioController @Inject constructor(@ApplicationContext ctx: Context) : Li controllerFuture.addListener({ seek(to) }, MoreExecutors.directExecutor()) } else { controller.seekTo(to) + if (!controller.isPlaying) { + controller.play() + } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt index 9814d10..078a839 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt @@ -28,9 +28,9 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.websocket.WebSockets import io.ktor.http.Url import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json import javax.inject.Named import javax.inject.Singleton -import kotlinx.serialization.json.Json @Module @InstallIn(SingletonComponent::class) 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..d618f84 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 @@ -35,7 +35,7 @@ const val ICON_SIZE = .8f @Composable fun PlayerGameBadge( imageUrl: String, - points: Int, + points: Int? = null, result: GuessResult? = null, size: Dp = 80.dp, modifier: Modifier = Modifier, @@ -76,12 +76,14 @@ fun PlayerGameBadge( } } } - 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), + ) + } } } 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..4e75054 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,94 @@ 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.graphics.Color +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 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}: ${200} points", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = Color.Black, + 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..62173d4 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 @@ -33,6 +33,6 @@ class GameResultViewModel @Inject constructor(private val gameDataStreamer: Game } private fun sortPlayers() { - _players.value = _players.value.sortedByDescending { player -> player.score } + _players.value = _players.value.sortedByDescending { player -> player.id } } } 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..b29895e 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 @@ -5,10 +5,14 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement 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 @@ -17,6 +21,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -24,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 @@ -37,11 +43,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 +54,48 @@ 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 + .plus(uiState.players) + .plus(uiState.players) + .plus(uiState.players) + .plus(uiState.players) + .plus(uiState.players) + .plus(uiState.players) + .plus(uiState.players) + ) { player -> + PlayerGameBadge(imageUrl = player.imageUrl, size = 40.dp) + } + } + + 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") } } @@ -77,8 +115,6 @@ fun GuessSongScreen( HorizontalDivider() - Text(text = "${uiState.songDuration} ${playbackState.progress}") - AudioPlayerControls( value = playbackState.progress, onValueChange = { viewModel.seek(it) }, @@ -124,5 +160,5 @@ fun SongItem(song: Song, onClick: () -> Unit) { @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..327ab26 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 @@ -9,6 +9,9 @@ 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 kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -20,6 +23,8 @@ data class GuessSongState( val query: String = "", val songs: List = listOf(), val songDuration: Long = 0, + val pointsToWin: Int = 0, + val cumulatedPoints: Int = 0, ) @HiltViewModel @@ -44,6 +49,7 @@ constructor( players = gameState?.players ?: listOf(), songs = gameState?.songs ?: listOf(), songDuration = gameState?.audio?.duration?.toMillis() ?: 0, + cumulatedPoints = gameState?.pointsMap?.get(gameState.me) ?: 0, ) } } @@ -62,6 +68,19 @@ constructor( 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) } + } + } } fun pause() { 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..21ef214 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,25 +1,17 @@ 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 @@ -31,64 +23,9 @@ 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, 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/navigation/GameGraph.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/GameGraph.kt index bda7449..9dbd9e2 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,9 +12,7 @@ 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) From d5eb4758a07bd5a761a2104f953a7f7d9870f9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Tue, 14 Jan 2025 01:27:58 +0100 Subject: [PATCH 2/3] Finis game flow implementation --- .../androidclient/game/datastreaming/Game.kt | 33 ++- .../game/datastreaming/GameDataStreamer.kt | 3 + .../datastreaming/RemoteGameDataStreamer.kt | 66 +++++- .../messages/server/CorrectSongMessage.kt | 9 + .../messages/server/EndGameMessage.kt | 14 ++ .../messages/server/ServerMessage.kt | 4 +- .../androidclient/game/model/GameState.kt | 1 + .../game/model/GuessCorrectness.kt | 3 +- .../game/model/PlayerFinalScore.kt | 9 + .../androidclient/infra/di/AppModule.kt | 2 + .../androidclient/ui/app/game/GuessResult.kt | 6 - .../ui/app/game/components/PlayerGameBadge.kt | 26 ++- .../app/game/gameresult/GameResultScreen.kt | 14 +- .../game/gameresult/GameResultViewModel.kt | 34 ++-- .../app/game/guesssong/GuessResultScreen.kt | 189 +++++++++--------- .../ui/app/game/guesssong/GuessSongScreen.kt | 27 +-- .../app/game/guesssong/GuessSongViewModel.kt | 46 ++++- .../ui/app/navigation/AppGraph.kt | 33 +++ .../ui/app/navigation/NavigationViewModel.kt | 11 +- 19 files changed, 370 insertions(+), 160 deletions(-) create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/CorrectSongMessage.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/EndGameMessage.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/game/model/PlayerFinalScore.kt delete mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/GuessResult.kt 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 cf26112..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 @@ -44,20 +44,23 @@ class Game(private var gameState: GameState) { fun markGuess(id: String) { gameState = gameState.copy( - songGuessMap = gameState.songGuessMap.plus(Pair(id, GuessCorrectness.VERIFYING)) + songGuessMap = gameState.songGuessMap.plus(Pair(id, GuessCorrectness.VERIFYING)), + lastGuessStatus = GuessCorrectness.VERIFYING, ) } fun resolveGuess(songId: String, correct: Boolean) { gameState = gameState.copy( - playerGuessMap = + songGuessMap = gameState.songGuessMap.plus( Pair( songId, if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT, ) - ) + ), + lastGuessStatus = + if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT, ) } @@ -83,4 +86,28 @@ class Game(private var gameState: GameState) { ) ) } + + 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 894322f..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,6 +13,8 @@ interface GameDataStreamer { fun gameStateFlow(): StateFlow + fun lastGameResultStateFlow(): StateFlow> + suspend fun updateSettings(settings: RoomSettings) suspend fun sendReadyStatus(ready: Boolean) 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 8dfd097..bfbebae 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 @@ -6,6 +6,8 @@ import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.G 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 @@ -17,6 +19,8 @@ import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.R 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 @@ -53,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 = @@ -97,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() @@ -115,11 +124,14 @@ constructor( return } + game it.setMyReadyStatus(ready) gameStateFlow.value = it.gameState() } } + resultStateFlow.value = listOf() + withContext(Dispatchers.IO) { networkClient.sendMessage(Json.encodeToString(ReadyStatusMessage(payload = ready))) } @@ -133,6 +145,11 @@ constructor( } it.markGuess(id) + + if (points == 0) { + it.setPlayerGuessResult(it.gameState().me ?: "", false) + } + gameStateFlow.value = it.gameState() } } @@ -160,6 +177,8 @@ constructor( 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) { @@ -192,6 +211,7 @@ constructor( pointsMap = initialState.players.associateBy({ it.id }, { 0 }), songGuessMap = mapOf(), playerGuessMap = mapOf(), + lastGuessStatus = GuessCorrectness.VERIFYING, ) ) gameStateFlow.value = game?.gameState() @@ -235,15 +255,21 @@ constructor( val start = Instant.ofEpochSecond(payload.timestamp) synchronized(this) { - game?.scheduleAudio(payload.url, start, Duration.ofMillis(payload.duration)) - gameStateFlow.value = game?.gameState() + 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 - Log.d("yupi", "yay") synchronized(this) { game.let { if (game == null) { @@ -251,12 +277,40 @@ constructor( } if (payload.songId.isNotEmpty()) { - game?.resolveGuess(payload.songId, payload.correct) + 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 } - game?.setPlayerGuessResult(payload.playerId, payload.correct) - game?.addPoints(payload.playerId, payload.points) + it.resetGame() + gameStateFlow.value = it.gameState() } } + + resultStateFlow.value = payload.results } } 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/ServerMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerMessage.kt index a7a9831..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,7 +10,9 @@ enum class ServerMessageType { PLAYER_READY, ROOM_STAGE, PLAY_SONG, - PLAYER_GUESS + 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 0b625fc..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 @@ -15,4 +15,5 @@ data class GameState( 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 index c150fd1..9f75b5d 100644 --- 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 @@ -1,7 +1,8 @@ package com.github.feelbeatapp.androidclient.game.model enum class GuessCorrectness { + UNKNOWN, + VERIFYING, CORRECT, INCORRECT, - VERIFYING, } 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/di/AppModule.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt index 078a839..e2c690c 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt @@ -67,6 +67,7 @@ abstract class AppModule { @Singleton @Named("API_URL") fun provideApiUrl(): String { +// return "http://192.168.100.14:3000/api/v1" return BuildConfig.API_URL } @@ -74,6 +75,7 @@ abstract class AppModule { @Singleton @Named("SOCKET_URI") fun provideSocketUri(): Url { +// return Url("ws://192.168.100.14:3000") return Url(BuildConfig.SOCKET_URI) } 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 d618f84..bc79618 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 @@ -36,7 +36,7 @@ const val ICON_SIZE = .8f fun PlayerGameBadge( imageUrl: String, points: Int? = null, - result: GuessResult? = null, + result: GuessCorrectness = GuessCorrectness.UNKNOWN, size: Dp = 80.dp, modifier: Modifier = Modifier, ) { @@ -55,9 +55,9 @@ 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 -> Color.Green.copy(alpha = .7f) + GuessCorrectness.INCORRECT -> Color.Red.copy(alpha = .7f) + else -> Color.Transparent } Column( @@ -65,14 +65,20 @@ 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), + ) } } } @@ -105,7 +111,7 @@ fun PlayerBadgeCorrectPreview() { PlayerGameBadge( imageUrl = "https://cdn-icons-png.flaticon.com/512/219/219983.png", points = 200, - result = GuessResult.CORRECT, + result = GuessCorrectness.CORRECT, ) } } @@ -117,7 +123,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 4e75054..95ea4d2 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 @@ -38,7 +38,7 @@ fun GameResultScreen( onNavigate: (String) -> Unit, viewModel: GameResultViewModel = hiltViewModel(), ) { - val players by viewModel.players.collectAsState() + val gameResultState by viewModel.gameResultState.collectAsState() Column( modifier = Modifier.fillMaxSize().padding(16.dp), @@ -56,11 +56,15 @@ fun GameResultScreen( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(8.dp), ) { - items(players) { player -> PlayerScoreItem(player = player) } + items(gameResultState.results.sortedByDescending { it.points }) { result -> + PlayerScoreItem(player = result.profile, score = result.points) + } } Button( - onClick = { onNavigate(AppRoute.HOME.name) }, + onClick = { + onNavigate(AppRoute.ROOM_LOBBY.withArgs(mapOf("roomId" to gameResultState.roomId))) + }, modifier = Modifier.padding(vertical = 16.dp), ) { Text(text = "CLOSE") @@ -69,7 +73,7 @@ fun GameResultScreen( } @Composable -fun PlayerScoreItem(player: Player) { +fun PlayerScoreItem(player: Player, score: Int) { Box(modifier = Modifier.fillMaxWidth().padding(8.dp), contentAlignment = Alignment.Center) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { AsyncImage( @@ -81,7 +85,7 @@ fun PlayerScoreItem(player: Player) { ) Text( - text = "${player.name}: ${200} points", + text = "${player.name}: $score points", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold, color = Color.Black, 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 62173d4..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.id } + 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..4e2a22f 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,26 +1,34 @@ 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( @@ -28,99 +36,94 @@ fun GuessResultScreen( 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") - // } - // } -} + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val playbackState by viewModel.playbackState.collectAsStateWithLifecycle() -@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), - ) + 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, + result = playerWithResult.status, + ) + } + } + } + + val correctSong = uiState.songs.find { it.status == GuessCorrectness.CORRECT }?.song + if (correctSong != null) { + ElevatedCard(modifier = Modifier.padding(48.dp).fillMaxWidth().weight(1f)) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(.8f).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() }, + ) } } 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 b29895e..d6ca0ac 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 @@ -36,6 +36,7 @@ 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 @@ -62,17 +63,8 @@ fun GuessSongScreen(viewModel: GuessSongViewModel = hiltViewModel()) { horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().weight(1f), ) { - items( - uiState.players - .plus(uiState.players) - .plus(uiState.players) - .plus(uiState.players) - .plus(uiState.players) - .plus(uiState.players) - .plus(uiState.players) - .plus(uiState.players) - ) { player -> - PlayerGameBadge(imageUrl = player.imageUrl, size = 40.dp) + items(uiState.players.plus(uiState.players)) { p -> + PlayerGameBadge(imageUrl = p.player.imageUrl, size = 40.dp, result = p.status) } } @@ -110,7 +102,13 @@ fun GuessSongScreen(viewModel: GuessSongViewModel = hiltViewModel()) { verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize().weight(1f), ) { - items(uiState.songs) { song -> SongItem(song = song, onClick = {}) } + items(uiState.songs) { s -> + SongItem( + song = s.song, + onClick = { viewModel.guess(s.song.id) }, + correctness = s.status, + ) + } } HorizontalDivider() @@ -145,7 +143,10 @@ fun SearchBar(searchQuery: String, onSearchQueryChange: (String) -> Unit) { } @Composable -fun SongItem(song: Song, onClick: () -> Unit) { +fun SongItem(song: Song, onClick: () -> Unit, correctness: GuessCorrectness) { + if (correctness == GuessCorrectness.VERIFYING) { + Text("Verifying") + } SongCard( title = song.title, artist = song.artist, 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 327ab26..7dc4f54 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,6 +4,7 @@ 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 @@ -19,14 +20,20 @@ import kotlinx.coroutines.launch import me.xdrop.fuzzywuzzy.FuzzySearch data class GuessSongState( - val players: List = listOf(), + val players: List = listOf(), val query: String = "", - val songs: List = listOf(), + val songs: List = listOf(), val songDuration: Long = 0, val pointsToWin: Int = 0, val cumulatedPoints: Int = 0, + val pointsMap: Map = mapOf(), + val lastGuessCorrect: Boolean = false, ) +data class SongState(val song: Song, val status: GuessCorrectness) + +data class PlayerState(val player: Player, val status: GuessCorrectness) + @HiltViewModel class GuessSongViewModel @Inject @@ -46,19 +53,36 @@ 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, + ) + } + ?.filter { song -> song.status != GuessCorrectness.INCORRECT } ?: listOf(), songDuration = gameState?.audio?.duration?.toMillis() ?: 0, cumulatedPoints = gameState?.pointsMap?.get(gameState.me) ?: 0, + pointsMap = gameState?.pointsMap ?: mapOf(), + lastGuessCorrect = gameState?.lastGuessStatus == GuessCorrectness.CORRECT, ) } } - 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()) } } @@ -79,6 +103,12 @@ constructor( 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 + } } } } @@ -90,4 +120,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/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/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() } From c5e7b32c4f1e06937fa8a8cbaba91e27864c5c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Tue, 14 Jan 2025 05:03:59 +0100 Subject: [PATCH 3/3] Perform UI/UX enhance --- .../datastreaming/RemoteGameDataStreamer.kt | 6 ++ .../infra/audio/AudioController.kt | 15 ++- .../androidclient/infra/di/AppModule.kt | 4 +- .../androidclient/ui/app/AppScreen.kt | 12 +-- .../ui/app/components/PlayerCard.kt | 4 +- .../ui/app/game/components/PlayerGameBadge.kt | 9 +- .../app/game/gameresult/GameResultScreen.kt | 6 +- .../app/game/guesssong/GuessResultScreen.kt | 17 ++-- .../ui/app/game/guesssong/GuessSongScreen.kt | 90 +++++++++++++----- .../app/game/guesssong/GuessSongViewModel.kt | 23 ++--- .../components/AudioPlayerControls.kt | 2 - .../ui/app/game/startgame/StartGameScreen.kt | 7 +- .../ui/app/lobby/lobbyhome/LobbyHomeScreen.kt | 4 +- .../ui/app/navigation/GameGraph.kt | 4 +- .../screens/LobbyRoomSettingsScreen.kt | 3 - .../screens/NewRoomSettingsScreen.kt | 3 - .../androidclient/ui/loading/LoadingScreen.kt | 37 ++++--- .../androidclient/ui/login/AuthActivity.kt | 3 +- .../androidclient/ui/login/LoginScreen.kt | 45 ++++++++- .../androidclient/ui/theme/Theme.kt | 51 +++++----- app/src/main/res/drawable/logo.xml | 21 ++++ app/src/main/res/drawable/userimage.webp | Bin 26296 -> 0 bytes app/src/main/res/values/strings.xml | 2 +- 23 files changed, 230 insertions(+), 138 deletions(-) create mode 100644 app/src/main/res/drawable/logo.xml delete mode 100644 app/src/main/res/drawable/userimage.webp 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 bfbebae..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 @@ -143,6 +143,12 @@ constructor( if (it == null) { return } + if ( + it.gameState().songGuessMap.getOrDefault(id, GuessCorrectness.UNKNOWN) != + GuessCorrectness.UNKNOWN + ) { + return + } it.markGuess(id) 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 ee876db..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 @@ -10,10 +12,13 @@ import androidx.media3.session.SessionToken import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject + +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) @@ -89,5 +94,13 @@ class AudioController @Inject constructor(@ApplicationContext ctx: Context) : Li 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/infra/di/AppModule.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt index e2c690c..9814d10 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/di/AppModule.kt @@ -28,9 +28,9 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.websocket.WebSockets import io.ktor.http.Url import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json import javax.inject.Named import javax.inject.Singleton +import kotlinx.serialization.json.Json @Module @InstallIn(SingletonComponent::class) @@ -67,7 +67,6 @@ abstract class AppModule { @Singleton @Named("API_URL") fun provideApiUrl(): String { -// return "http://192.168.100.14:3000/api/v1" return BuildConfig.API_URL } @@ -75,7 +74,6 @@ abstract class AppModule { @Singleton @Named("SOCKET_URI") fun provideSocketUri(): Url { -// return Url("ws://192.168.100.14:3000") return Url(BuildConfig.SOCKET_URI) } 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/components/PlayerGameBadge.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/game/components/PlayerGameBadge.kt index bc79618..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 @@ -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,8 +55,9 @@ fun PlayerGameBadge( val color = when (result) { - GuessCorrectness.CORRECT -> Color.Green.copy(alpha = .7f) - GuessCorrectness.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 } 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 95ea4d2..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 @@ -21,7 +21,6 @@ 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.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -78,8 +77,8 @@ fun PlayerScoreItem(player: Player, score: Int) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { AsyncImage( model = player.imageUrl, - placeholder = painterResource(R.drawable.userimage), - error = painterResource(R.drawable.userimage), + placeholder = painterResource(R.drawable.account), + error = painterResource(R.drawable.account), contentDescription = stringResource(R.string.player_avatar), modifier = Modifier.size(48.dp).clip(CircleShape), ) @@ -88,7 +87,6 @@ fun PlayerScoreItem(player: Player, score: Int) { text = "${player.name}: $score points", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold, - color = Color.Black, modifier = Modifier.padding(start = 16.dp), ) } 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 4e2a22f..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 @@ -30,12 +30,10 @@ 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 +const val CARD_WIDTH = .8f + @Composable -fun GuessResultScreen( - roomId: String, - onNavigate: (String) -> Unit, - viewModel: GuessSongViewModel = hiltViewModel(), -) { +fun GuessResultScreen(viewModel: GuessSongViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val playbackState by viewModel.playbackState.collectAsStateWithLifecycle() @@ -61,6 +59,7 @@ fun GuessResultScreen( PlayerGameBadge( imageUrl = playerWithResult.player.imageUrl, points = uiState.pointsMap[playerWithResult.player.id] ?: 0, + size = 40.dp, result = playerWithResult.status, ) } @@ -69,7 +68,9 @@ fun GuessResultScreen( val correctSong = uiState.songs.find { it.status == GuessCorrectness.CORRECT }?.song if (correctSong != null) { - ElevatedCard(modifier = Modifier.padding(48.dp).fillMaxWidth().weight(1f)) { + ElevatedCard( + modifier = Modifier.fillMaxWidth().heightIn(0.dp, 500.dp).weight(1f).padding(24.dp) + ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth(), @@ -77,7 +78,7 @@ fun GuessResultScreen( Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth(.8f).heightIn(0.dp, 300.dp), + modifier = Modifier.fillMaxWidth(CARD_WIDTH).heightIn(0.dp, 300.dp), ) { AsyncImage( model = correctSong.imageUrl, @@ -130,5 +131,5 @@ fun GuessResultScreen( @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 d6ca0ac..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,8 +1,11 @@ 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 @@ -14,19 +17,23 @@ 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 @@ -63,7 +70,7 @@ fun GuessSongScreen(viewModel: GuessSongViewModel = hiltViewModel()) { horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().weight(1f), ) { - items(uiState.players.plus(uiState.players)) { p -> + items(uiState.players) { p -> PlayerGameBadge(imageUrl = p.player.imageUrl, size = 40.dp, result = p.status) } } @@ -102,11 +109,13 @@ fun GuessSongScreen(viewModel: GuessSongViewModel = hiltViewModel()) { verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize().weight(1f), ) { - items(uiState.songs) { s -> + 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) }, ) } } @@ -125,37 +134,68 @@ fun GuessSongScreen(viewModel: GuessSongViewModel = hiltViewModel()) { @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, correctness: GuessCorrectness) { - if (correctness == GuessCorrectness.VERIFYING) { - Text("Verifying") +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, + ) + } } - SongCard( - title = song.title, - artist = song.artist, - imageUrl = song.imageUrl, - duration = song.duration, - onClick = onClick, - size = 50.dp, - displayDuration = false, - ) } @Preview 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 7dc4f54..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 @@ -9,8 +9,6 @@ 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 kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -18,16 +16,20 @@ 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 query: String = "", 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) @@ -61,19 +63,14 @@ constructor( ) } ?: listOf(), songs = - gameState - ?.songs - ?.map { song -> - SongState( - song, - gameState.songGuessMap[song.id] ?: GuessCorrectness.UNKNOWN, - ) - } - ?.filter { song -> song.status != GuessCorrectness.INCORRECT } ?: listOf(), + 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, ) } } @@ -86,6 +83,10 @@ constructor( } } + 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)) } } 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 21ef214..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 @@ -18,7 +18,6 @@ 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 @@ -64,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/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/GameGraph.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/GameGraph.kt index 9dbd9e2..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 @@ -14,9 +14,7 @@ fun NavGraphBuilder.gameGraph(onNavigate: (String) -> Unit) { 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/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 10641c541f43f7b948a03086dbb36f3c8f88d1d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26296 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 z 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≤ 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%0AYW-B(hKmBDw_p&!Qj`@#y`^=J$zgh@=MB= zP(7kp>Z6v4Coaz+0=zwbB0e9}mNY=yP#>*leb`xCz5T#kITF{pO4OaX(ONw zfzz3aQ8B)9^_P#0jUB2`p;Gm~;p0Xe@t5x% z8(Vt)ZFW1jOP>R;>|i6ZRQ~Z5VuV~$+G{Ps=Og2a#y}f`Xx;u8&T?%xlo%g-gtWk# zg!yRM=%C*vq#n;X@VfWq^s!|QgP<|g)UpKcsTL+m#X^ja(AbHZnH^_6)-$P~OI-S~ zJ8=47l&5Lm2CbVgrJN<)<)CxN7Ubib?tKk2qs>+CCa50&&w{)@_B_e=!P|hk0L~J2 z@0KLU$45`rs)IR^D?=?&cU_gE`f{8;qN;}*8l|<&myx%G+Z`p$N4I!sCt64KO_&oH&^YFMkLyxHq$N1U+VZ# z`V;Q0M?y%W0AsL-y^({4?=^w&>=!yxfVZkUYu^*CGiUPSFLhj=LNZP=d%N9dxux1P z@UK4Nb;Rc!o-mvMZ$TlQd`BF8j;$l2)Kdf}$?*2*`S#zf&+aPHz0ckDpD$cS&h1N& zB*NRk^|fz{)-j0}6;b}YACN0mWwG4G!YD? z{n3eWpUUU#ehj#m|bj;G8rF`jqWS@98CE&E0E=vS| z{G+t*O|(wxmS4;gp=vzHz2!4SGJaNOP_D&=-6eFHkhRT{PhIVnVS>2Y`

l=1?w1(ppq^&@azvIm{0` z?(Q!otXuompKlIXnMAn)*PLG1w}B3`?EPJ4Mvt@SKPyZWmmL2551K@NreXbpo=G1h zZd<@3t`?lboQMq06D96TZMBD;X9TB-VN@*-c^IPKZ(*B=ZuP^BlhsG3%FHwAg4d9+ zH*A-aBEY-iD3>}K62i= z82nl$wAb_85A@3aKTtK78VwUOjFkG()<_^=#yQNZzGfCjjlTQB5Tm6p{{z;n-$|tm z%C4Su>I2Jo%RmYxs`t=oTzHp|Z|nK7QVY}KSGCdsXU}RQ{C3G#Dsxk>qJJ>CruX;t zA&ovy3)N|0IQaCjTYe0oS{8Yc$KH?G`2n@EOjBN@-;%|5K5hL>UH+{f^6pnAjB?`Q zTBn)e@O$XtHjT<0Yamx%re)fFdoQs5rI)(!hhg098W7<_!r_GgUjnQEbvQJx%VY8%udVguli7}(^QZ$n0(fOa+xcK zE%^OE`vILMiMO*bCq%@XC4LhPe?@1sS)wt_OH2BGYx?^8}cS6glcWZXmiH< zaP?1LCzn}J;mB^OeAC^ue92y6y40NL_JJ&+@bHHC34h3uluqK{=olNB2FxEC0g%UyTGo4+L(Zk zzTP*|673tt8`xyjrs?)yf3C3xs^Wj@e_?Ozdbd2jabs^oTkAG%+_-Sw?dn{1Couldn\'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