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 6c2c5cc..6cfb1ed 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.GameState import com.github.feelbeatapp.androidclient.game.model.Player +import com.github.feelbeatapp.androidclient.game.model.RoomStage class Game(private var gameState: GameState) { fun gameState(): GameState { @@ -19,4 +20,16 @@ class Game(private var gameState: GameState) { fun setAdmin(playerId: String) { gameState = gameState.copy(adminId = playerId) } + + fun setMyReadyStatus(ready: Boolean) { + gameState = gameState.copy(readyMap = gameState.readyMap.plus(Pair(gameState.me, ready))) + } + + fun updateReadyStatus(playerId: String, ready: Boolean) { + gameState = gameState.copy(readyMap = gameState.readyMap.plus(Pair(playerId, ready))) + } + + fun setStage(stage: RoomStage) { + gameState = gameState.copy(stage = stage) + } } 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 f1120ba..8b140f1 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.RoomSettings import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.StateFlow @@ -10,4 +11,8 @@ interface GameDataStreamer { fun leaveRoom() fun gameStateFlow(): 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 58862f7..3dde5f5 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,15 +1,25 @@ package com.github.feelbeatapp.androidclient.game.datastreaming import android.util.Log +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.InitialGameState import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.NewPlayerMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerLeftMessage +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerReadyMessage +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.RoomStageMessage +import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerErrorMessage import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerMessageType import com.github.feelbeatapp.androidclient.game.model.GameState +import com.github.feelbeatapp.androidclient.game.model.RoomSettings +import com.github.feelbeatapp.androidclient.game.model.RoomStage +import com.github.feelbeatapp.androidclient.infra.auth.AuthManager import com.github.feelbeatapp.androidclient.infra.error.ErrorCode import com.github.feelbeatapp.androidclient.infra.error.ErrorReceiver import com.github.feelbeatapp.androidclient.infra.error.FeelBeatException +import com.github.feelbeatapp.androidclient.infra.error.FeelBeatServerException import com.github.feelbeatapp.androidclient.infra.network.NetworkClient import javax.inject.Inject import kotlinx.coroutines.CancellationException @@ -23,14 +33,18 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive class RemoteGameDataStreamer @Inject -constructor(private val networkClient: NetworkClient, private val errorReceiver: ErrorReceiver) : - GameDataStreamer { +constructor( + private val networkClient: NetworkClient, + private val errorReceiver: ErrorReceiver, + private val authManager: AuthManager, +) : GameDataStreamer { private var game: Game? = null private var gameStateFlow = MutableStateFlow(null) private var scope: CoroutineScope? = null @@ -67,30 +81,56 @@ constructor(private val networkClient: NetworkClient, private val errorReceiver: override fun leaveRoom() { scope?.cancel() scope = null - gameStateFlow.value = null + synchronized(this) { + game = null + gameStateFlow.value = null + } } override fun gameStateFlow(): StateFlow { return gameStateFlow.asStateFlow() } - private fun processMessage(content: String) { + override suspend fun updateSettings(settings: RoomSettings) { + withContext(Dispatchers.IO) { + val token = authManager.getAccessToken() + val message = + SettingsUpdateMessage( + payload = SettingsUpdatePayload(token = token, settings = settings) + ) + networkClient.sendMessage(Json.encodeToString(message)) + } + } + + override suspend fun sendReadyStatus(ready: Boolean) { + synchronized(this) { + game.let { + if (it == null) { + return + } + + it.setMyReadyStatus(ready) + gameStateFlow.value = it.gameState() + } + } + + withContext(Dispatchers.IO) { + networkClient.sendMessage(Json.encodeToString(ReadyStatusMessage(payload = ready))) + } + } + + private suspend fun processMessage(content: String) { try { val type = Json.decodeFromString(content)["type"]?.jsonPrimitive?.content when (type) { + ServerMessageType.SERVER_ERROR.name -> processServerError(content) ServerMessageType.INITIAL.name -> loadInitialState(Json.decodeFromString(content).payload) - ServerMessageType.NEW_PLAYER.name -> { - game?.addPlayer(Json.decodeFromString(content).payload) - gameStateFlow.value = game?.gameState() - } - ServerMessageType.PLAYER_LEFT.name -> { - val payload = Json.decodeFromString(content).payload - game?.removePlayer(payload.left) - game?.setAdmin(payload.admin) - gameStateFlow.value = game?.gameState() - } + ServerMessageType.NEW_PLAYER.name -> processNewPlayer(content) + ServerMessageType.PLAYER_LEFT.name -> processPlayerLeft(content) + ServerMessageType.PLAYER_READY.name -> processPlayerReady(content) + ServerMessageType.ROOM_STAGE.name -> processRoomStage(content) else -> Log.w("RemoteGameDataStreamer", "Received unexpected message: $content") } } catch (e: Exception) { @@ -98,6 +138,13 @@ constructor(private val networkClient: NetworkClient, private val errorReceiver: } } + private suspend fun processServerError(content: String) { + errorReceiver.submitError( + FeelBeatServerException(Json.decodeFromString(content).payload) + ) + } + + @Synchronized private fun loadInitialState(initialState: InitialGameState) { game = Game( @@ -109,8 +156,44 @@ constructor(private val networkClient: NetworkClient, private val errorReceiver: me = initialState.me, players = initialState.players, songs = initialState.playlist.songs.map { it.toSongModel() }, + settings = initialState.settings, + readyMap = initialState.readyMap, + stage = RoomStage.LOBBY, ) ) gameStateFlow.value = game?.gameState() } + + @Synchronized + private fun processNewPlayer(content: String) { + game?.addPlayer(Json.decodeFromString(content).payload) + gameStateFlow.value = game?.gameState() + } + + private fun processPlayerLeft(content: String) { + val payload = Json.decodeFromString(content).payload + synchronized(this) { + game?.removePlayer(payload.left) + game?.setAdmin(payload.admin) + gameStateFlow.value = game?.gameState() + } + } + + private fun processPlayerReady(content: String) { + val payload = Json.decodeFromString(content).payload + + synchronized(this) { + game?.updateReadyStatus(payload.player, payload.ready) + gameStateFlow.value = game?.gameState() + } + } + + private fun processRoomStage(content: String) { + val stage = Json.decodeFromString(content).payload + + synchronized(this) { + game?.setStage(RoomStage.valueOf(stage)) + gameStateFlow.value = game?.gameState() + } + } } 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 new file mode 100644 index 0000000..4af22a8 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ClientMessageType.kt @@ -0,0 +1,6 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client + +enum class ClientMessageType { + SETTINGS_UPDATE, + READY_STATUS, +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ReadyStatusMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ReadyStatusMessage.kt new file mode 100644 index 0000000..0753ab8 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/ReadyStatusMessage.kt @@ -0,0 +1,10 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client + +import kotlinx.serialization.Required +import kotlinx.serialization.Serializable + +@Serializable +data class ReadyStatusMessage( + @Required val type: String = ClientMessageType.READY_STATUS.name, + val payload: Boolean, +) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/SettingsUpdateMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/SettingsUpdateMessage.kt new file mode 100644 index 0000000..eb44f32 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/client/SettingsUpdateMessage.kt @@ -0,0 +1,13 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client + +import com.github.feelbeatapp.androidclient.game.model.RoomSettings +import kotlinx.serialization.Required +import kotlinx.serialization.Serializable + +@Serializable +data class SettingsUpdateMessage( + @Required val type: String = ClientMessageType.SETTINGS_UPDATE.name, + val payload: SettingsUpdatePayload, +) + +@Serializable data class SettingsUpdatePayload(val settings: RoomSettings, val token: String) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/InitialMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/InitialMessage.kt index 1203569..93f23e1 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/InitialMessage.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/InitialMessage.kt @@ -2,9 +2,9 @@ package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server import com.github.feelbeatapp.androidclient.game.model.Player import com.github.feelbeatapp.androidclient.game.model.RoomSettings +import kotlin.time.Duration.Companion.seconds import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.time.Duration.Companion.seconds @Serializable @SerialName("INITIAL") @@ -20,6 +20,7 @@ data class InitialGameState( val admin: String, val playlist: Playlist, val players: List, + val readyMap: Map, val settings: RoomSettings, ) @@ -43,4 +44,3 @@ data class Song( ) } } - diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerReadyMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerReadyMessage.kt new file mode 100644 index 0000000..3e81f1e --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/PlayerReadyMessage.kt @@ -0,0 +1,13 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("PLAYER_READY") +data class PlayerReadyMessage( + override val type: String = ServerMessageType.PLAYER_READY.name, + val payload: PlayerReadyPayload, +) : ServerMessage() + +@Serializable data class PlayerReadyPayload(val player: String, val ready: Boolean) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/RoomStageMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/RoomStageMessage.kt new file mode 100644 index 0000000..124bfd5 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/RoomStageMessage.kt @@ -0,0 +1,10 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server + +import kotlinx.serialization.Required +import kotlinx.serialization.Serializable + +@Serializable +data class RoomStageMessage( + @Required override val type: String = ServerMessageType.ROOM_STAGE.name, + val payload: String, +) : ServerMessage() diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerErrorMessage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerErrorMessage.kt new file mode 100644 index 0000000..225e1be --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/datastreaming/messages/server/ServerErrorMessage.kt @@ -0,0 +1,14 @@ +package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +@SerialName("SERVER_ERROR") +data class ServerErrorMessage( + override val type: String = ServerMessageType.SERVER_ERROR.name, + val payload: String +) : ServerMessage() + + 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 2c96dd3..0cd9c67 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 @@ -6,6 +6,9 @@ enum class ServerMessageType { INITIAL, NEW_PLAYER, PLAYER_LEFT, + SERVER_ERROR, + PLAYER_READY, + ROOM_STAGE, } @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 d2ec060..75db785 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 @@ -8,4 +8,7 @@ data class GameState( val me: String, val players: List, val songs: List, + val settings: RoomSettings, + val readyMap: Map, + val stage: RoomStage, ) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/RoomStage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/RoomStage.kt new file mode 100644 index 0000000..98b4c7c --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/game/model/RoomStage.kt @@ -0,0 +1,6 @@ +package com.github.feelbeatapp.androidclient.game.model + +enum class RoomStage { + LOBBY, + GAME, +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/error/ErrorCode.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/error/ErrorCode.kt index 97c6485..3aa82ae 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/error/ErrorCode.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/error/ErrorCode.kt @@ -11,7 +11,8 @@ enum class ErrorCode { FEELBEAT_SERVER_ERROR, FEELBEAT_SERVER_INCORRECT_RESPONSE_FORMAT, INCORRECT_PLAYLIST_LINK, - FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM; + FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM, + MAX_PLAYERS_TOO_SMALL; fun toStringId(): Int { return when (this) { @@ -20,6 +21,7 @@ enum class ErrorCode { INCORRECT_PLAYLIST_LINK -> R.string.incorrect_playlist_link FEELBEAT_SERVER_ERROR -> R.string.feelbeat_server_error FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM -> R.string.failed_to_join_room + MAX_PLAYERS_TOO_SMALL -> R.string.max_players_too_small else -> R.string.unexpected_error } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/network/WebsocketClient.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/network/WebsocketClient.kt index aef857f..5c601f4 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/infra/network/WebsocketClient.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/infra/network/WebsocketClient.kt @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.withContext /** Websocket implementation of communication with FeelBeat server */ class WebsocketClient @@ -47,6 +49,7 @@ constructor( path = "${serverUrl.fullPath}${path}", request = { header("Authorization", "Bearer $token") }, ) { + session = this for (msg in offlineQueue) { this.send(msg) } @@ -65,6 +68,7 @@ constructor( .catch { e -> throw FeelBeatException(ErrorCode.FEELBEAT_SERVER_FAILED_TO_JOIN_ROOM, e) } + .onCompletion { session = null } } override suspend fun disconnect() { @@ -72,10 +76,12 @@ constructor( } override suspend fun sendMessage(text: String) { - if (session == null) { - offlineQueue.offer(text) - } else { - session?.send(text) + withContext(Dispatchers.IO) { + if (session == null) { + offlineQueue.offer(text) + } else { + session?.send(text) + } } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppViewModel.kt index ff8143f..546f214 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/AppViewModel.kt @@ -28,7 +28,7 @@ constructor( private val authManager: AuthManager, private val spotifyAPI: SpotifyAPI, private val errorEmitter: ErrorEmitter, - private val gameDataStreamer: GameDataStreamer + private val gameDataStreamer: GameDataStreamer, ) : ViewModel() { private val _playerIdentity = MutableStateFlow(null) val playerIdentity = _playerIdentity.asStateFlow() 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 19320ae..d54f157 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 @@ -1,7 +1,11 @@ package com.github.feelbeatapp.androidclient.ui.app.components +import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -13,25 +17,35 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.github.feelbeatapp.androidclient.R import com.github.feelbeatapp.androidclient.game.model.Player +import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable -fun PlayerCard(player: Player, size: Dp = 80.dp, modifier: Modifier = Modifier) { +fun PlayerCard( + player: Player, + size: Dp = 80.dp, + modifier: Modifier = Modifier, + block: @Composable () -> Unit = {}, +) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { - AsyncImage( - model = player.imageUrl, - error = painterResource(R.drawable.userimage), - placeholder = painterResource(R.drawable.userimage), - contentDescription = stringResource(R.string.player_image), - modifier = - Modifier.size(size) - .clip(CircleShape) - .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape), - ) + Box(modifier = Modifier.size(size).clip(CircleShape)) { + AsyncImage( + model = player.imageUrl, + error = painterResource(R.drawable.userimage), + placeholder = painterResource(R.drawable.userimage), + contentDescription = stringResource(R.string.player_image), + modifier = + Modifier.size(size) + .clip(CircleShape) + .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape), + ) + block() + } Text( text = player.name, style = MaterialTheme.typography.bodyMedium, @@ -40,3 +54,28 @@ fun PlayerCard(player: Player, size: Dp = 80.dp, modifier: Modifier = Modifier) ) } } + +@Composable +@Preview(showBackground = true) +fun PlayerCardReadyPreview() { + FeelBeatTheme { + PlayerCard( + player = + Player( + id = "", + name = "Player name", + imageUrl = "https://cdn-icons-png.flaticon.com/512/219/219983.png", + ) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.fillMaxSize() + .background(MaterialTheme.colorScheme.primary.copy(alpha = .8f)), + ) { + Text("Ready!", color = MaterialTheme.colorScheme.onPrimary) + } + } + } +} 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 837a7b5..9b2c1b1 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 @@ -1,5 +1,6 @@ package com.github.feelbeatapp.androidclient.ui.app.lobby.lobbyhome +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -38,7 +39,7 @@ import com.github.feelbeatapp.androidclient.ui.loading.LoadingScreen @OptIn(ExperimentalLayoutApi::class) @Composable -fun LobbyHomeScreen(onPlay: () -> Unit, viewModel: LobbyHomeViewModel = hiltViewModel()) { +fun LobbyHomeScreen(viewModel: LobbyHomeViewModel = hiltViewModel()) { val lobbyState by viewModel.lobbyHomeState.collectAsStateWithLifecycle() if (lobbyState.currentRoomId == null) { @@ -86,13 +87,40 @@ fun LobbyHomeScreen(onPlay: () -> Unit, viewModel: LobbyHomeViewModel = hiltView verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth().padding(16.dp), ) { - lobbyState.players.forEach { player -> PlayerCard(player = player) } + lobbyState.players.forEach { player -> + PlayerCard(player = player) { + if (lobbyState.readyMap.getOrDefault(player.id, false)) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.fillMaxSize() + .background(MaterialTheme.colorScheme.primary.copy(alpha = .8f)), + ) { + Text("Ready!", color = MaterialTheme.colorScheme.onPrimary) + } + } + } } Spacer(modifier = Modifier.height(32.dp)) - Button(onClick = onPlay, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { - Text(stringResource(R.string.play), style = MaterialTheme.typography.headlineMedium) + val ready = lobbyState.readyMap[lobbyState.me] ?: false + Button( + onClick = {viewModel.setReadyToPlay(!ready)}, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) { + Text( + text = + if (!ready) stringResource(R.string.ready) + else + stringResource( + R.string.waiting_for, + lobbyState.readyMap.count { it.value }, + lobbyState.players.size, + ), + style = MaterialTheme.typography.headlineMedium, + ) } } } @@ -100,5 +128,5 @@ fun LobbyHomeScreen(onPlay: () -> Unit, viewModel: LobbyHomeViewModel = hiltView @Preview(showBackground = true) @Composable fun PreviewAcceptScreen() { - LobbyHomeScreen({}) + LobbyHomeScreen() } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeViewModel.kt index 6e30872..4d4a553 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/lobby/lobbyhome/LobbyHomeViewModel.kt @@ -6,11 +6,11 @@ import com.github.feelbeatapp.androidclient.game.datastreaming.GameDataStreamer import com.github.feelbeatapp.androidclient.game.model.GameState import com.github.feelbeatapp.androidclient.game.model.Player import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject data class LobbyHomeState( val currentRoomId: String? = null, @@ -18,6 +18,8 @@ data class LobbyHomeState( val playlistImageUrl: String = "", val adminName: String = "Admin", val players: List = listOf(), + val readyMap: Map = mapOf(), + val me: String = "", ) @HiltViewModel @@ -40,9 +42,15 @@ class LobbyHomeViewModel @Inject constructor(private val gameDataStreamer: GameD playlistImageUrl = gameState.playlistImageUrl, adminName = gameState.players.find { it.id == gameState.adminId }?.name ?: "", players = gameState.players, + readyMap = gameState.readyMap, + me = gameState.me, ) } else { _lobbyHomeState.update { it.copy(currentRoomId = null) } } } + + fun setReadyToPlay(ready: Boolean) { + viewModelScope.launch { gameDataStreamer.sendReadyStatus(ready) } + } } 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 e22ab4d..7d922fe 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 @@ -15,6 +15,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.game.model.RoomStage import com.github.feelbeatapp.androidclient.ui.app.AppScreen import com.github.feelbeatapp.androidclient.ui.app.home.HomeScreen import com.github.feelbeatapp.androidclient.ui.app.lobby.components.LobbyBottomBar @@ -23,11 +24,17 @@ import com.github.feelbeatapp.androidclient.ui.app.roomsettings.screens.NewRoomS import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable -fun AppGraph(onLogout: () -> Unit, lobbyViewModel: LobbyViewModel = hiltViewModel()) { +fun AppGraph( + onLogout: () -> Unit, + lobbyViewModel: LobbyViewModel = hiltViewModel(), + navigationViewModel: NavigationViewModel = hiltViewModel(), +) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() val route = navBackStackEntry?.destination?.route val lobbyState by lobbyViewModel.lobbyState.collectAsStateWithLifecycle() + val stage by navigationViewModel.stage.collectAsStateWithLifecycle(RoomStage.LOBBY) + val loadedRoomId by navigationViewModel.roomId.collectAsStateWithLifecycle(null) LaunchedEffect(lobbyState.joinFailed) { if (lobbyState.joinFailed) { @@ -36,6 +43,16 @@ fun AppGraph(onLogout: () -> Unit, lobbyViewModel: LobbyViewModel = hiltViewMode } } + LaunchedEffect(stage) { + if (stage == RoomStage.GAME) { + navController.navigate( + AppRoute.START_GAME.withArgs(mapOf("roomId" to (loadedRoomId ?: ""))) + ) { + popUpTo(AppRoute.HOME.route) + } + } + } + AppScreen( title = getRouteTitle(route), backVisible = getBackButtonVisibility(route), @@ -77,16 +94,7 @@ fun AppGraph(onLogout: () -> Unit, lobbyViewModel: LobbyViewModel = hiltViewMode ) } navigation(route = AppRoute.ROOM.route, startDestination = AppRoute.ROOM_LOBBY.route) { - lobbyGraph( - onPlay = { - val roomId = navBackStackEntry?.getRoomId() - if (roomId != null) { - navController.navigate( - AppRoute.START_GAME.withArgs(mapOf("roomId" to roomId)) - ) - } - }, - ) + lobbyGraph() } navigation(route = AppRoute.GAME.route, startDestination = AppRoute.START_GAME.route) { diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/LobbyGraph.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/LobbyGraph.kt index 5f3bc85..3c69ad0 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/LobbyGraph.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/LobbyGraph.kt @@ -6,8 +6,8 @@ import com.github.feelbeatapp.androidclient.ui.app.lobby.lobbyhome.LobbyHomeScre import com.github.feelbeatapp.androidclient.ui.app.lobby.lobbysongs.LobbySongsScreen import com.github.feelbeatapp.androidclient.ui.app.roomsettings.screens.LobbyRoomSettingsScreen -fun NavGraphBuilder.lobbyGraph(onPlay: () -> Unit) { - composable(route = AppRoute.ROOM_LOBBY.route) { LobbyHomeScreen(onPlay = onPlay) } +fun NavGraphBuilder.lobbyGraph() { + composable(route = AppRoute.ROOM_LOBBY.route) { LobbyHomeScreen() } composable(route = AppRoute.ROOM_SONGS.route) { LobbySongsScreen() } 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 new file mode 100644 index 0000000..36ab137 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/navigation/NavigationViewModel.kt @@ -0,0 +1,14 @@ +package com.github.feelbeatapp.androidclient.ui.app.navigation + +import androidx.lifecycle.ViewModel +import com.github.feelbeatapp.androidclient.game.datastreaming.GameDataStreamer +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.map + +@HiltViewModel +class NavigationViewModel @Inject constructor(private val gameDataStreamer: GameDataStreamer) : + ViewModel() { + val stage = gameDataStreamer.gameStateFlow().map { it?.stage } + val roomId = gameDataStreamer.gameStateFlow().map { it?.roomId } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/components/SettingsControls.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/components/SettingsControls.kt index 84f0536..9195112 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/components/SettingsControls.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/components/SettingsControls.kt @@ -25,7 +25,7 @@ fun SettingsControls(viewModel: RoomSettingsViewModel, enabled: Boolean = true) label = stringResource(R.string.number_of_players), value = roomSettings.maxPlayers, onValueChange = { viewModel.setMaxPlayers(it) }, - valueRange = 1..7, + valueRange = 1..8, interval = 1, enabled = enabled, ) @@ -35,7 +35,7 @@ fun SettingsControls(viewModel: RoomSettingsViewModel, enabled: Boolean = true) value = roomSettings.turnCount, onValueChange = { viewModel.setTurnCount(it) }, valueRange = 1..10, - interval = 9, + interval = 1, enabled = enabled, ) @@ -44,7 +44,7 @@ fun SettingsControls(viewModel: RoomSettingsViewModel, enabled: Boolean = true) value = roomSettings.timePenaltyPerSecond, onValueChange = { viewModel.setTimePenaltyPerSecond(it) }, valueRange = 1..20, - interval = 19, + interval = 1, enabled = enabled, ) @@ -53,7 +53,7 @@ fun SettingsControls(viewModel: RoomSettingsViewModel, enabled: Boolean = true) value = roomSettings.basePoints, onValueChange = { viewModel.setBasePoints(it) }, valueRange = 100..1000, - interval = 9, + interval = 10, enabled = enabled, ) @@ -62,7 +62,7 @@ fun SettingsControls(viewModel: RoomSettingsViewModel, enabled: Boolean = true) value = roomSettings.incorrectGuessPenalty, onValueChange = { viewModel.setIncorrectGuessPenalty(it) }, valueRange = 50..500, - interval = 9, + interval = 50, enabled = enabled, ) 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 af5709f..f186d6e 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 @@ -2,20 +2,29 @@ package com.github.feelbeatapp.androidclient.ui.app.roomsettings.screens 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.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width 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 import androidx.compose.ui.Modifier +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 androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.github.feelbeatapp.androidclient.R import com.github.feelbeatapp.androidclient.ui.app.roomsettings.components.SettingsControls import com.github.feelbeatapp.androidclient.ui.app.roomsettings.viewmodels.LobbyRoomSettingsViewModel -import com.github.feelbeatapp.androidclient.ui.loading.LoadingScreen @Composable fun LobbyRoomSettingsScreen( @@ -23,17 +32,32 @@ fun LobbyRoomSettingsScreen( modifier: Modifier = Modifier, ) { val editRoomState by viewModel.editRoomState.collectAsStateWithLifecycle() - - if (!editRoomState.loaded) { - LoadingScreen() - return - } + val isApplied by viewModel.isApplied.collectAsStateWithLifecycle(true) Column( modifier = modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(24.dp), ) { SettingsControls(viewModel, enabled = editRoomState.isAdmin) + + 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), + ) + } + } else { + Button( + onClick = { viewModel.apply() }, + modifier = Modifier.fillMaxWidth(), + enabled = !isApplied, + ) { + Text(stringResource(R.string.apply)) + } + } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/LobbyRoomSettingsViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/LobbyRoomSettingsViewModel.kt index 550c8dd..7f2e128 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/LobbyRoomSettingsViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/LobbyRoomSettingsViewModel.kt @@ -3,28 +3,75 @@ package com.github.feelbeatapp.androidclient.ui.app.roomsettings.viewmodels 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.infra.error.ErrorCode +import com.github.feelbeatapp.androidclient.infra.error.ErrorReceiver +import com.github.feelbeatapp.androidclient.infra.error.FeelBeatException import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +const val BASE_PLAYLIST_LINK = "https://open.spotify.com/playlist" + data class EditRoomState(val isAdmin: Boolean = false, val loaded: Boolean = false) @HiltViewModel class LobbyRoomSettingsViewModel @Inject -constructor(private val gameDataStreamer: GameDataStreamer) : RoomSettingsViewModel() { +constructor( + private val gameDataStreamer: GameDataStreamer, + private val errorReceiver: ErrorReceiver, +) : RoomSettingsViewModel() { private val _editRoomState = MutableStateFlow(EditRoomState()) val editRoomState = _editRoomState.asStateFlow() + private var appliedSettings: RoomSettings + val isApplied = MutableStateFlow(true) + init { updateState(gameDataStreamer.gameStateFlow().value) + appliedSettings = mRoomSettings.value + viewModelScope.launch { gameDataStreamer.gameStateFlow().collect { updateState(it) } } + + viewModelScope.launch { roomSettings.collect { isApplied.value = appliedSettings == it } } } private fun updateState(gameState: GameState?) { _editRoomState.value = EditRoomState(isAdmin = gameState?.adminId == gameState?.me, loaded = gameState != null) + + if (gameState != null) { + appliedSettings = + RoomSettings( + maxPlayers = gameState.settings.maxPlayers, + turnCount = gameState.settings.turnCount, + basePoints = gameState.settings.basePoints, + incorrectGuessPenalty = gameState.settings.incorrectGuessPenalty, + timePenaltyPerSecond = gameState.settings.timePenaltyPerSecond, + playlistLink = "${BASE_PLAYLIST_LINK}/${gameState.settings.playlistId}", + ) + + mRoomSettings.value = appliedSettings + isApplied.value = true + } + } + + fun apply() { + val currentPlayersCount = gameDataStreamer.gameStateFlow().value?.players?.size ?: 0 + if (currentPlayersCount > roomSettings.value.maxPlayers) { + setMaxPlayers(currentPlayersCount) + viewModelScope.launch { + errorReceiver.submitError(FeelBeatException(ErrorCode.MAX_PLAYERS_TOO_SMALL)) + } + return + } + + _editRoomState.update { it.copy(loaded = false) } + viewModelScope.launch { + gameDataStreamer.updateSettings(roomSettings.value.toRoomSettingsModel()) + } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/NewRoomSettingsViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/NewRoomSettingsViewModel.kt index f453bdc..dff776a 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/NewRoomSettingsViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/NewRoomSettingsViewModel.kt @@ -38,7 +38,7 @@ constructor(private val feelBeatApi: FeelBeatApi, private val errorReceiver: Err _loading.value = true try { - val roomId = feelBeatApi.createRoom(roomSettings.value.toCreateRoomPayload()) + val roomId = feelBeatApi.createRoom(roomSettings.value.toRoomSettingsModel()) _roomCreated.emit(roomId) } catch (e: FeelBeatException) { errorReceiver.submitError(e) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/RoomSettingsViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/RoomSettingsViewModel.kt index 2bc98ce..5d6a07e 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/RoomSettingsViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/app/roomsettings/viewmodels/RoomSettingsViewModel.kt @@ -15,7 +15,7 @@ data class RoomSettings( val incorrectGuessPenalty: Int, val playlistLink: String, ) { - fun toCreateRoomPayload(): RoomSettings { + fun toRoomSettingsModel(): RoomSettings { return RoomSettings( maxPlayers = maxPlayers, turnCount = turnCount, @@ -28,40 +28,40 @@ data class RoomSettings( } abstract class RoomSettingsViewModel : ViewModel() { - private val _roomSettings = + protected val mRoomSettings = MutableStateFlow( RoomSettings( - maxPlayers = 0, + maxPlayers = 4, turnCount = 5, timePenaltyPerSecond = 5, - basePoints = 100, - incorrectGuessPenalty = 20, + basePoints = 500, + incorrectGuessPenalty = 100, playlistLink = "", ) ) - val roomSettings = _roomSettings.asStateFlow() + val roomSettings = mRoomSettings.asStateFlow() fun setMaxPlayers(value: Int) { - _roomSettings.update { it.copy(maxPlayers = value) } + mRoomSettings.update { it.copy(maxPlayers = value) } } fun setTurnCount(value: Int) { - _roomSettings.update { it.copy(turnCount = value) } + mRoomSettings.update { it.copy(turnCount = value) } } fun setTimePenaltyPerSecond(value: Int) { - _roomSettings.update { it.copy(timePenaltyPerSecond = value) } + mRoomSettings.update { it.copy(timePenaltyPerSecond = value) } } fun setBasePoints(value: Int) { - _roomSettings.update { it.copy(basePoints = value) } + mRoomSettings.update { it.copy(basePoints = value) } } fun setIncorrectGuessPenalty(value: Int) { - _roomSettings.update { it.copy(incorrectGuessPenalty = value) } + mRoomSettings.update { it.copy(incorrectGuessPenalty = value) } } fun setPlaylistLink(value: String) { - _roomSettings.update { it.copy(playlistLink = value) } + mRoomSettings.update { it.copy(playlistLink = value) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13a78f6..ffd2374 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,7 +8,7 @@ Edit room Feel Beat Back - PLAY + READY Player Image Snippet Duration: %1$d seconds Points to Win: %1$d @@ -43,4 +43,7 @@ Song cover Playlist content Couldn\'t join the room + Can\'t set max players, too much players in room + Apply + WAITING FOR %1$s/%2$s \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32b1256..8593d9f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.3" +agp = "8.8.0" coilCompose = "2.4.0" fuzzywuzzy = "1.4.0" kotlin = "2.0.21" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2d482d4..c153d7a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Nov 15 18:56:49 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists