diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt index 2d6cdb2..61a2499 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/AuthManager.kt @@ -13,5 +13,7 @@ interface AuthManager { suspend fun fetchAccessToken(code: String) + fun cancelLoginFlow() + suspend fun getAccessToken(): String } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt index 44769b7..0554e44 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/spotify/SpotifyAuthManager.kt @@ -107,6 +107,10 @@ constructor( onAuthenticated() } + override fun cancelLoginFlow() { + state = AuthState.NOT_AUTHENTICATED + } + override suspend fun getAccessToken(): String { var auth = checkNotNull(authData) { "Not authenticated" } if (hasExpired(auth.expires)) { diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt index 086a569..afcce35 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/auth/storage/PreferencesAuthStorage.kt @@ -1,13 +1,11 @@ package com.github.feelbeatapp.androidclient.auth.storage import android.content.Context -import android.content.Intent import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys import com.github.feelbeatapp.androidclient.auth.AuthData import dagger.hilt.android.qualifiers.ApplicationContext import java.time.Instant -import java.time.LocalDate import javax.inject.Inject class PreferencesAuthStorage @Inject constructor(@ApplicationContext ctx: Context) : AuthStorage { diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/AuthActivity.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/AuthActivity.kt index a24adf6..e755a92 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/AuthActivity.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/AuthActivity.kt @@ -27,6 +27,7 @@ class AuthActivity : ComponentActivity() { val code = intentUri.getQueryParameter("code") if (error != null || code == null) { + authManager.cancelLoginFlow() startActivity(Intent(this, MainActivity::class.java)) return } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt index 7430213..afed8be 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt @@ -8,29 +8,56 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.github.feelbeatapp.androidclient.ui.game.GameScreen +import com.github.feelbeatapp.androidclient.ui.acceptGame.AcceptGameScreen +import com.github.feelbeatapp.androidclient.ui.gameResult.GameResultScreen +import com.github.feelbeatapp.androidclient.ui.guessSong.GuessResultScreen +import com.github.feelbeatapp.androidclient.ui.guessSong.GuessSongScreen import com.github.feelbeatapp.androidclient.ui.home.HomeScreen import com.github.feelbeatapp.androidclient.ui.login.LoginScreen +import com.github.feelbeatapp.androidclient.ui.newRoomSettings.NewRoomSettingsScreen +import com.github.feelbeatapp.androidclient.ui.roomSettings.RoomSettingsScreen +import com.github.feelbeatapp.androidclient.ui.startGame.StartGameScreen import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable fun FeelBeatApp( @Suppress("UnusedParameter") widthSizeClass: WindowWidthSizeClass, - startRoute: FeelBeatRoute, + startScreen: FeelBeatRoute, modifier: Modifier = Modifier, ) { FeelBeatTheme { val navController = rememberNavController() Box(modifier = modifier) { - NavHost(navController, startDestination = startRoute.name) { + NavHost(navController, startDestination = startScreen.name) { composable(route = FeelBeatRoute.LOGIN.name) { LoginScreen() } - composable(route = FeelBeatRoute.HOME.name) { - HomeScreen(parentNavController = navController) + HomeScreen(navController = navController) + } + composable(route = FeelBeatRoute.NEW_ROOM_SETTINGS.name) { + NewRoomSettingsScreen(navController = navController) + } + composable(route = FeelBeatRoute.ROOM_SETTINGS.name) { + RoomSettingsScreen(navController = navController) + } + composable(route = FeelBeatRoute.ACCEPT_GAME.name) { + AcceptGameScreen(navController = navController) + } + composable(route = FeelBeatRoute.ACCOUNT_SETTINGS.name) { + // AccountSettingsScreen(parentNavController = navController) + } + composable(route = FeelBeatRoute.GAME_RESULT.name) { + GameResultScreen(navController = navController) + } + composable(route = FeelBeatRoute.GUESS_SONG.name) { + GuessSongScreen(navController = navController) + } + composable(route = FeelBeatRoute.GUESS_RESULT.name) { + GuessResultScreen(navController = navController) + } + composable(route = FeelBeatRoute.START_GAME.name) { + StartGameScreen(navController = navController) } - - composable(route = FeelBeatRoute.GAME.name) { GameScreen() } } } } @@ -39,5 +66,5 @@ fun FeelBeatApp( @Preview @Composable fun AppPreview() { - FeelBeatApp(WindowWidthSizeClass.Compact, FeelBeatRoute.HOME) + FeelBeatApp(WindowWidthSizeClass.Compact, FeelBeatRoute.LOGIN) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatRoute.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatRoute.kt index b8d3755..c62cac5 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatRoute.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatRoute.kt @@ -3,6 +3,12 @@ package com.github.feelbeatapp.androidclient.ui enum class FeelBeatRoute { LOGIN, HOME, - LOBBY, - GAME, + NEW_ROOM_SETTINGS, + ROOM_SETTINGS, + ACCEPT_GAME, + ACCOUNT_SETTINGS, + GAME_RESULT, + GUESS_SONG, + GUESS_RESULT, + START_GAME, } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/MainActivity.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/MainActivity.kt index b0ae052..2a2e3fb 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/MainActivity.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/MainActivity.kt @@ -7,26 +7,21 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import com.github.feelbeatapp.androidclient.auth.AuthManager -import com.github.feelbeatapp.androidclient.auth.AuthState -import com.github.feelbeatapp.androidclient.network.fullduplex.NetworkAgent import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject lateinit var socket: NetworkAgent @Inject lateinit var authManager: AuthManager @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() -// socket.connect("/ws") val startRoute = - if (authManager.isAuthenticated()) FeelBeatRoute.HOME - else FeelBeatRoute.LOGIN + if (authManager.isAuthenticated()) FeelBeatRoute.HOME else FeelBeatRoute.LOGIN + enableEdgeToEdge() setContent { val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass FeelBeatApp(widthSizeClass, startRoute) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameScreen.kt new file mode 100644 index 0000000..aa67308 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameScreen.kt @@ -0,0 +1,164 @@ +package com.github.feelbeatapp.androidclient.ui.acceptGame + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +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.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute +import com.github.feelbeatapp.androidclient.ui.startGame.PlayerCard + +@Composable +fun AcceptGameScreen( + viewModel: AcceptGameViewModel = AcceptGameViewModel(), + navController: NavController, + isRoomCreator: Boolean = true, +) { + val gameState = viewModel.gameState.collectAsState().value + + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { + Column( + modifier = + Modifier.fillMaxSize() + .padding(bottom = 56.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = stringResource(R.string.back), + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + gameState.players.forEach { player -> PlayerCard(player = player) } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(horizontal = 16.dp), + ) { + Text( + text = + stringResource( + id = R.string.snippet_duration_val, + gameState.snippetDuration, + ), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = stringResource(id = R.string.points_to_win_val, gameState.pointsToWin), + style = MaterialTheme.typography.bodyMedium, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(id = R.string.playlist_name, gameState.playlist.name), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + gameState.playlist.songs.forEach { song -> SongItem(song = song) } + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { navController.navigate(FeelBeatRoute.START_GAME.name) }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) { + Text(stringResource(R.string.play), style = MaterialTheme.typography.headlineMedium) + } + } + + if (isRoomCreator) { + BottomNavigationBar( + navController = navController, + modifier = Modifier.align(Alignment.BottomCenter), + ) + } + } +} + +@Composable +fun BottomNavigationBar(navController: NavController, modifier: Modifier = Modifier) { + NavigationBar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + ) { + NavigationBarItem( + icon = { + Icon(Icons.Filled.Home, contentDescription = stringResource(R.string.selected_room)) + }, + label = { Text(stringResource(R.string.selected_room)) }, + selected = false, + onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + ) + NavigationBarItem( + icon = { + Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) + }, + label = { Text(stringResource(R.string.settings)) }, + selected = false, + onClick = { navController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }, + ) + } +} + +@Composable +fun SongItem(song: Song) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = song.title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + ) + } +} + +@Preview +@Composable +fun PreviewAcceptScreen() { + val navController = rememberNavController() + AcceptGameScreen(navController = navController) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameViewModel.kt new file mode 100644 index 0000000..fd8fed5 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameViewModel.kt @@ -0,0 +1,63 @@ +package com.github.feelbeatapp.androidclient.ui.acceptGame + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.home.Room +import com.github.feelbeatapp.androidclient.ui.startGame.Player +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 Song(val id: Int, val title: String) + +data class Playlist(val name: String, val songs: List) + +data class GameState( + val players: List = emptyList(), + val songs: List = emptyList(), + val selectedRoom: Room? = null, + val playlist: Playlist = Playlist(name = "Playlist #1", songs = emptyList()), + val snippetDuration: Int = 30, + val pointsToWin: Int = 10, +) + +class AcceptGameViewModel : ViewModel() { + private val _gameState = MutableStateFlow(GameState()) + val gameState: StateFlow = _gameState.asStateFlow() + + init { + loadPlayers() + loadSongs() + } + + private fun loadPlayers() { + viewModelScope.launch { + val examplePlayers = + listOf( + Player("User123", R.drawable.userimage), + Player("User456", R.drawable.userimage), + Player("User789", R.drawable.userimage), + ) + + _gameState.update { it.copy(players = examplePlayers) } + } + } + + @SuppressWarnings("MagicNumber") + private fun loadSongs() { + viewModelScope.launch { + val exampleSongs = + listOf( + Song(1, "Song 1"), + Song(2, "Song 2"), + Song(3, "Song 3"), + Song(4, "Song 4"), + Song(5, "Song 5"), + ) + _gameState.update { it.copy(songs = exampleSongs) } + } + } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameScreen.kt index 8c1cb31..bc64426 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.material3.TextField 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 diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameViewModel.kt index 3bb631e..485f738 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/game/GameViewModel.kt @@ -21,9 +21,7 @@ class GameViewModel @Inject constructor(private val socket: NetworkAgent) : View init { viewModelScope.launch(Dispatchers.IO) { - socket.receiveFlow().collect { msg -> - _state.update { GameState(msg) } - } + socket.receiveFlow().collect { msg -> _state.update { GameState(msg) } } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultScreen.kt new file mode 100644 index 0000000..7f9f12f --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultScreen.kt @@ -0,0 +1,96 @@ +package com.github.feelbeatapp.androidclient.ui.gameResult + +import androidx.compose.foundation.Image +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute +import com.github.feelbeatapp.androidclient.ui.guessSong.PlayerWithResult + +@Composable +fun GameResultScreen( + navController: NavController, + viewModel: GameResultViewModel = GameResultViewModel(), +) { + 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 = { navController.navigate(FeelBeatRoute.HOME) }, + modifier = Modifier.padding(vertical = 16.dp), + ) { + Text(text = "CLOSE") + } + } +} + +@Composable +fun PlayerScoreItem(player: PlayerWithResult) { + Box(modifier = Modifier.fillMaxWidth().padding(8.dp), contentAlignment = Alignment.Center) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(R.drawable.userimage), + contentDescription = "Player Avatar", + modifier = Modifier.size(48.dp).clip(CircleShape), + ) + Text( + text = "${player.player.name}: ${player.points} points", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier.padding(start = 16.dp), + ) + } + } +} + +@Preview +@Composable +fun GameResultPreview() { + val navController = rememberNavController() + GameResultScreen(navController = navController) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultViewModel.kt new file mode 100644 index 0000000..911df3c --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultViewModel.kt @@ -0,0 +1,48 @@ +package com.github.feelbeatapp.androidclient.ui.gameResult + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.guessSong.PlayerWithResult +import com.github.feelbeatapp.androidclient.ui.guessSong.ResultStatus +import com.github.feelbeatapp.androidclient.ui.startGame.Player +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class GameResultViewModel : ViewModel() { + + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players + + init { + fetchGameResults() + } + + @SuppressWarnings("MagicNumber") + private fun fetchGameResults() { + viewModelScope.launch { + val results = + listOf( + PlayerWithResult( + Player("User123", R.drawable.userimage), + ResultStatus.CORRECT, + 10, + ), + PlayerWithResult( + Player("User456", R.drawable.userimage), + ResultStatus.WRONG, + 7, + ), + PlayerWithResult( + Player("User789", R.drawable.userimage), + ResultStatus.NORESPONSE, + 1, + ), + ) + .sortedByDescending { it.points } + + _players.value = results + } + } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessResultScreen.kt new file mode 100644 index 0000000..087ea07 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessResultScreen.kt @@ -0,0 +1,136 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute + +@Composable +fun GuessResultScreen( + navController: NavController, + viewModel: GuessSongViewModel = GuessSongViewModel(), +) { + 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.image, + 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) + }, + 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 = { navController.navigate(FeelBeatRoute.GUESS_SONG.name) }) { + Text(text = "NEXT") + } + } +} + +@Composable +fun PlayerStatusIcon(image: Int, isCorrect: Boolean) { + Box(contentAlignment = Alignment.TopEnd) { + Image( + painter = painterResource(id = image), + 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), + ) + } +} + +@Composable +fun SongInfo(songTitle: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column { + Text( + text = songTitle, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + ) + } + } +} + +@Preview +@Composable +fun GuessResultScreenPreview() { + val nav = rememberNavController() + GuessResultScreen(navController = nav) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongScreen.kt new file mode 100644 index 0000000..3c75008 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongScreen.kt @@ -0,0 +1,206 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +import androidx.compose.foundation.Image +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.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.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute +import com.github.feelbeatapp.androidclient.ui.acceptGame.Song +import kotlinx.coroutines.delay + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GuessSongScreen( + navController: NavController, + viewModel: GuessSongViewModel = GuessSongViewModel(), +) { + val guessState by viewModel.guessState.collectAsState() + var timeLeft by remember { mutableIntStateOf(guessState.snippetDuration) } + + LaunchedEffect(key1 = timeLeft) { + if (timeLeft > 0) { + delay(timeMillis = 1000) + timeLeft -= 1 + } else { + navController.navigate(FeelBeatRoute.GUESS_RESULT.name) + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(guessState.playlist.name) }, + actions = { + Text( + text = timeLeft.toString(), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(end = 16.dp), + ) + }, + ) + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues).fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + guessState.players.forEach { playerWithResult -> + PlayerStatusIcon(player = playerWithResult) + } + } + + MusicControlSlider() + + Column { + Text( + text = stringResource(R.string.guess_the_song), + style = MaterialTheme.typography.bodyMedium, + ) + SearchBar( + searchQuery = guessState.searchQuery, + onSearchQueryChange = { viewModel.updateSearchQuery(it) }, + ) + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxSize(), + ) { + items(guessState.songs.size) { index -> + SongItem( + song = guessState.songs[index], + onClick = { navController.navigate(FeelBeatRoute.GUESS_RESULT.name) }, + ) + } + } + } + } +} + +@Composable +fun PlayerStatusIcon(player: PlayerWithResult) { + val icon = + when (player.resultStatus) { + ResultStatus.CORRECT -> Icons.Outlined.Done + ResultStatus.WRONG -> Icons.Outlined.Close + ResultStatus.NORESPONSE -> null + } + val color = + when (player.resultStatus) { + ResultStatus.CORRECT -> Color.Green + ResultStatus.WRONG -> Color.Red + ResultStatus.NORESPONSE -> Color.Gray + } + + Box(modifier = Modifier.size(48.dp)) { + Image( + painter = painterResource(id = player.player.image), + contentDescription = stringResource(R.string.player_avatar), + modifier = Modifier.size(48.dp).clip(CircleShape), + ) + icon?.let { + Icon( + imageVector = it, + tint = color, + contentDescription = null, + modifier = Modifier.align(Alignment.BottomEnd).size(16.dp), + ) + } + } +} + +@Composable +fun MusicControlSlider() { + Column { + Text(stringResource(R.string.music_control), style = MaterialTheme.typography.bodyMedium) + Slider( + value = 0.5f, + onValueChange = { /* TODO Handle slider change */ }, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +fun SearchBar(searchQuery: TextFieldValue, onSearchQueryChange: (TextFieldValue) -> Unit) { + Row( + modifier = + Modifier.fillMaxWidth() + .border(1.dp, Color.Gray, MaterialTheme.shapes.small) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + 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) { + Card(onClick = onClick, modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { Text(song.title, style = MaterialTheme.typography.bodyLarge) } + } + } +} + +@Preview +@Composable +fun GuessSongPreview() { + val nav = rememberNavController() + GuessSongScreen(navController = nav) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongViewModel.kt new file mode 100644 index 0000000..e7a49ce --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongViewModel.kt @@ -0,0 +1,105 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.acceptGame.Playlist +import com.github.feelbeatapp.androidclient.ui.acceptGame.Song +import com.github.feelbeatapp.androidclient.ui.home.Room +import com.github.feelbeatapp.androidclient.ui.startGame.Player +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class PlayerWithResult(val player: Player, val resultStatus: ResultStatus, val points: Int) + +data class GuessState( + val players: List = emptyList(), + val songs: List = emptyList(), + val selectedRoom: Room? = null, + val playlist: Playlist = Playlist(name = "Playlist #1", songs = emptyList()), + val snippetDuration: Int = 30, + val pointsToWin: Int = 10, + val searchQuery: TextFieldValue = TextFieldValue(""), + val currentSong: Song? = null, +) + +class GuessSongViewModel : ViewModel() { + private val _guessState = MutableStateFlow(GuessState()) + val guessState: StateFlow = _guessState.asStateFlow() + + init { + loadPlayers() + loadPlaylist() + } + + private fun loadPlayers() { + viewModelScope.launch { + val examplePlayers = + listOf( + PlayerWithResult( + Player("User123", R.drawable.userimage), + ResultStatus.CORRECT, + 0, + ), + PlayerWithResult( + Player("User456", R.drawable.userimage), + ResultStatus.WRONG, + 0, + ), + PlayerWithResult( + Player("User789", R.drawable.userimage), + ResultStatus.NORESPONSE, + 0, + ), + ) + _guessState.value = _guessState.value.copy(players = examplePlayers) + } + } + + @SuppressWarnings("MagicNumber") + private fun loadPlaylist() { + viewModelScope.launch { + val examplePlaylist = + listOf( + Song(1, "Song 1"), + Song(2, "Song 2"), + Song(3, "Song 3"), + Song(4, "Song 4"), + Song(5, "Song 5"), + Song(6, "Song 6"), + ) + _guessState.value = + _guessState.value.copy(songs = examplePlaylist, currentSong = examplePlaylist[0]) + } + } + + fun updateSearchQuery(newQuery: TextFieldValue) { + _guessState.value = _guessState.value.copy(searchQuery = newQuery) + } + + fun submitAnswer(playerName: String, isCorrect: Boolean) { + viewModelScope.launch { + val updatedPlayers = + _guessState.value.players.map { playerWithResult -> + if (playerWithResult.player.name == playerName) { + val newPoints = playerWithResult.points + if (isCorrect) 1 else 0 + playerWithResult.copy( + resultStatus = + if (isCorrect) ResultStatus.CORRECT else ResultStatus.WRONG, + points = newPoints, + ) + } else { + playerWithResult + } + } + _guessState.value = _guessState.value.copy(players = updatedPlayers) + } + } + + fun setCurrentSong(song: Song) { + _guessState.value = _guessState.value.copy(currentSong = song) + } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultStatus.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultStatus.kt new file mode 100644 index 0000000..aa06694 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultStatus.kt @@ -0,0 +1,7 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +enum class ResultStatus { + CORRECT, + WRONG, + NORESPONSE, +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeRoute.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeRoute.kt deleted file mode 100644 index 25d6ce9..0000000 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeRoute.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.feelbeatapp.androidclient.ui.home - -enum class HomeRoute { - HOME, - CHOOSE_PLAYLIST, - GAME_SETTINGS, -} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt index e0474a6..a02ebf4 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeScreen.kt @@ -1,21 +1,25 @@ package com.github.feelbeatapp.androidclient.ui.home +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.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape +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.outlined.Notifications +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.outlined.Person -import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -23,100 +27,73 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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.platform.LocalContext 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.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import coil3.compose.AsyncImage import com.github.feelbeatapp.androidclient.R -import com.github.feelbeatapp.androidclient.error.errorCodeToStringResource import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute -import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable fun HomeScreen( - parentNavController: NavHostController, - homeViewModel: HomeViewModel = hiltViewModel(), + viewModel: HomeViewModel = HomeViewModel(), modifier: Modifier = Modifier, + navController: NavController, ) { - val navController = rememberNavController() - val currentBackStack by navController.currentBackStackEntryAsState() - val profile by homeViewModel.profile.collectAsState() - val profileErr by homeViewModel.error.collectAsState() - val title = currentBackStack?.destination?.route ?: "FeelBeat" - - LaunchedEffect(Unit) { homeViewModel.triggerProfileLoading() } + val title = stringResource(R.string.feel_beat) + val rooms by viewModel.rooms.collectAsState() + val selectedRoom by viewModel.selectedRoom.collectAsState() - Scaffold(topBar = { HomeTopBar(title) }) { innerPadding -> + Scaffold(topBar = { HomeTopBar(title, navController) }) { innerPadding -> Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { - NavHost(navController, startDestination = HomeRoute.HOME.name) { - composable(route = HomeRoute.HOME.name) { Text("Here list of games") } - - composable(route = HomeRoute.CHOOSE_PLAYLIST.name) { Text("Here choose playlist") } - - composable(route = HomeRoute.GAME_SETTINGS.name) { - Text("Here choose game settings") - } - } - - Row { - Button(onClick = { navController.navigate(HomeRoute.HOME.name) }) { Text("Home") } - - Button(onClick = { navController.navigate(HomeRoute.CHOOSE_PLAYLIST.name) }) { - Text("Playlists") - } - - Button(onClick = { navController.navigate(HomeRoute.GAME_SETTINGS.name) }) { - Text("Settings") + Text( + text = stringResource(R.string.current_games), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(16.dp), + ) + LazyColumn( + modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(rooms) { room -> + RoomItem( + room = room, + isSelected = room == selectedRoom, + onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + ) } } - HorizontalDivider() - - Button(onClick = { parentNavController.navigate(FeelBeatRoute.GAME.name) }) { - Text("Game") - } - - HorizontalDivider() - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().padding(0.dp, 20.dp), + Box( + modifier = + Modifier.fillMaxWidth().padding(bottom = 80.dp, start = 16.dp, end = 16.dp) ) { - if (profile == null && profileErr == null) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - strokeWidth = 8.dp, - modifier = Modifier.width(50.dp).height(50.dp), - ) - } else if (profileErr != null) { - Text(errorCodeToStringResource(LocalContext.current, checkNotNull(profileErr))) - } else { - Text(profile?.displayName ?: "") - Text(profile?.email ?: "") - - AsyncImage( - model = profile?.images?.get(0)?.url, - contentDescription = "profile pic", - modifier = - Modifier.padding(0.dp, 20.dp) - .clip(CircleShape) - .width(300.dp) - .height(300.dp), - ) + Box( + modifier = + Modifier.align(Alignment.BottomEnd) + .offset(x = (-15).dp) + .size(60.dp) + .background( + MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.medium, + ) + ) { + IconButton( + onClick = { navController.navigate(FeelBeatRoute.NEW_ROOM_SETTINGS.name) }, + modifier = Modifier.fillMaxSize(), + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add", + modifier = Modifier.size(36.dp), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } } } } @@ -125,7 +102,7 @@ fun HomeScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeTopBar(title: String) { +fun HomeTopBar(title: String, navController: NavController) { CenterAlignedTopAppBar( title = { Text(title) }, colors = @@ -133,16 +110,8 @@ fun HomeTopBar(title: String) { containerColor = MaterialTheme.colorScheme.primaryContainer, titleContentColor = MaterialTheme.colorScheme.primary, ), - navigationIcon = { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Outlined.Notifications, - contentDescription = stringResource(R.string.notifications), - ) - } - }, actions = { - IconButton(onClick = {}) { + IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCOUNT_SETTINGS.name) }) { Icon( imageVector = Icons.Outlined.Person, contentDescription = stringResource(R.string.account), @@ -152,11 +121,38 @@ fun HomeTopBar(title: String) { ) } +@Composable +fun RoomItem(room: Room, isSelected: Boolean, onClick: () -> Unit) { + Card( + onClick = onClick, + colors = + CardDefaults.cardColors( + containerColor = + if (isSelected) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surface + ), + modifier = + Modifier.fillMaxWidth() + .padding(8.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = MaterialTheme.shapes.medium, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(16.dp)) { + Text( + text = room.name, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + ) + } + } +} + @Preview @Composable fun HomePreview() { - FeelBeatTheme { - val nav = rememberNavController() - HomeScreen(nav) - } + val navController = rememberNavController() + HomeScreen(navController = navController) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt index cd7735f..840eee0 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeViewModel.kt @@ -1,42 +1,64 @@ package com.github.feelbeatapp.androidclient.ui.home -import android.util.Log import androidx.lifecycle.ViewModel -import com.github.feelbeatapp.androidclient.error.ErrorCode -import com.github.feelbeatapp.androidclient.error.FeelBeatException -import com.github.feelbeatapp.androidclient.network.spotify.SpotifyAPI -import com.github.feelbeatapp.androidclient.network.spotify.responses.ProfileResponse -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class HomeViewModel @Inject constructor(private val spotifyAPI: SpotifyAPI) : ViewModel() { - private val _profile = MutableStateFlow(null) - private val _error = MutableStateFlow(null) - val profile = _profile.asStateFlow() - val error = _error.asStateFlow() - - fun triggerProfileLoading() { - if (profile.value != null) { - return - } - CoroutineScope(Dispatchers.IO).launch { - try { - val profileResponse = spotifyAPI.getProfile() - _profile.update { profileResponse } - } catch (e: FeelBeatException) { - _error.update { e.code } - Log.e("HomeViewModel", e.message.toString()) - } catch (e: Exception) { - Log.e("HomeViewModel", "Unhandled exception: ${e.message}") - } +data class Room( + val id: Int, + val name: String, + val maxPlayers: Int, + val snippetDuration: Int, + val pointsToWin: Int, + val playlistLink: String, +) + +class HomeViewModel : ViewModel() { + + private val _rooms = MutableStateFlow>(emptyList()) + val rooms: StateFlow> = _rooms.asStateFlow() + + private val _selectedRoom = MutableStateFlow(null) + val selectedRoom: StateFlow = _selectedRoom.asStateFlow() + + init { + loadRooms() + } + + @SuppressWarnings("MagicNumber") + private fun loadRooms() { + viewModelScope.launch { + val exampleRooms = + listOf( + Room(1, "Pokój 1", 4, 30, 10, "https://example.com/playlist1"), + Room(2, "Pokój 2", 5, 20, 5, "https://example.com/playlist2"), + Room(3, "Pokój 3", 2, 25, 3, "https://example.com/playlist3"), + Room(4, "Pokój 4", 4, 30, 4, "https://example.com/playlist4"), + Room(5, "Pokój 5", 5, 25, 4, "https://example.com/playlist5"), + Room(6, "Pokój 6", 2, 25, 10, "https://example.com/playlist6"), + Room(7, "Pokój 7", 4, 30, 9, "https://example.com/playlist7"), + ) + _rooms.value = exampleRooms } } + + fun selectRoom(room: Room) { + _selectedRoom.value = room + } + + fun addRoom(name: String) { + val newRoom = + Room( + id = _rooms.value.size + 1, + name = name, + maxPlayers = 4, + snippetDuration = 30, + pointsToWin = 10, + playlistLink = "link", + ) + _rooms.value += newRoom + } } 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 5804697..becf38c 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 @@ -14,10 +14,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.github.feelbeatapp.androidclient.R @Composable -fun LoginScreen( - modifier: Modifier = Modifier, - loginViewModel: LoginViewModel = hiltViewModel(), -) { +fun LoginScreen(loginViewModel: LoginViewModel = hiltViewModel(), modifier: Modifier = Modifier) { val ctx = LocalContext.current Column( @@ -25,7 +22,7 @@ fun LoginScreen( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.fillMaxSize(), ) { - Text("Login screen") + Text(stringResource(R.string.login_screen)) Button(onClick = { loginViewModel.login(ctx) }) { Text(stringResource(R.string.login_with_spotify)) } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsScreen.kt new file mode 100644 index 0000000..58174e7 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsScreen.kt @@ -0,0 +1,155 @@ +package com.github.feelbeatapp.androidclient.ui.newRoomSettings + +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.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBar +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NewRoomSettingsScreen( + viewModel: NewRoomSettingsViewModel = NewRoomSettingsViewModel(), + navController: NavController, + modifier: Modifier = Modifier, +) { + val playlistLink by viewModel.playlistLink.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.new_room)) }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = stringResource(R.string.back), + ) + } + }, + ) + }, + content = { padding -> + Column( + modifier = modifier.fillMaxSize().padding(padding).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + SettingSliders(viewModel = viewModel) + + Text( + text = stringResource(R.string.playlist_link), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp), + ) + + TextField( + value = playlistLink, + onValueChange = { viewModel.setPlaylistLink(it) }, + modifier = Modifier.fillMaxWidth().height(56.dp).padding(bottom = 16.dp), + label = { Text(stringResource(R.string.enter_playlist_link)) }, + ) + + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier.fillMaxWidth().height(56.dp), + ) { + Text(stringResource(R.string.create_room)) + } + } + }, + ) +} + +@Composable +fun SettingSliders(viewModel: NewRoomSettingsViewModel) { + val maxPlayers by viewModel.maxPlayers.collectAsState() + val snippetDuration by viewModel.snippetDuration.collectAsState() + val pointsToWin by viewModel.pointsToWin.collectAsState() + + SettingSlider( + label = stringResource(R.string.number_of_players), + value = maxPlayers, + onValueChange = { viewModel.setMaxPlayers(it.toInt()) }, + valueRange = 1..5, + steps = 4, + ) + + SettingSlider( + label = stringResource(R.string.snippet_duration), + value = snippetDuration, + onValueChange = { viewModel.setSnippetDuration(it.toInt()) }, + valueRange = 5..30, + steps = 5, + ) + + SettingSlider( + label = stringResource(R.string.points_to_win), + value = pointsToWin, + onValueChange = { viewModel.setPointsToWin(it.toInt()) }, + valueRange = 3..10, + steps = 6, + ) +} + +@Composable +fun SettingSlider( + label: String, + value: Int, + onValueChange: (Int) -> Unit, + valueRange: IntRange, + steps: Int, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = label, style = MaterialTheme.typography.bodyMedium) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Slider( + value = value.toFloat(), + onValueChange = { onValueChange(it.toInt()) }, + valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(), + steps = steps - 1, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(16.dp)) + Text(text = value.toString(), style = MaterialTheme.typography.bodyMedium) + } + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Composable +fun PreviewSettingsScreen() { + val navController = rememberNavController() + NewRoomSettingsScreen(navController = navController) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsViewModel.kt new file mode 100644 index 0000000..e8bde69 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsViewModel.kt @@ -0,0 +1,37 @@ +package com.github.feelbeatapp.androidclient.ui.newRoomSettings + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class NewRoomSettingsViewModel : ViewModel() { + private val _maxPlayers = MutableStateFlow(0) + val maxPlayers: StateFlow = _maxPlayers.asStateFlow() + + private val _snippetDuration = MutableStateFlow(0) + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() + + private val _pointsToWin = MutableStateFlow(0) + val pointsToWin: StateFlow = _pointsToWin.asStateFlow() + + private val _playlistLink = MutableStateFlow("") + val playlistLink: StateFlow + get() = _playlistLink.asStateFlow() + + fun setMaxPlayers(value: Int) { + _maxPlayers.value = value + } + + fun setSnippetDuration(value: Int) { + _snippetDuration.value = value + } + + fun setPointsToWin(value: Int) { + _pointsToWin.value = value + } + + fun setPlaylistLink(value: String) { + _playlistLink.value = value + } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsScreen.kt new file mode 100644 index 0000000..0d68aee --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsScreen.kt @@ -0,0 +1,160 @@ +package com.github.feelbeatapp.androidclient.ui.roomSettings + +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.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute + +@Composable +fun RoomSettingsScreen( + viewModel: RoomSettingsViewModel = RoomSettingsViewModel(), + navController: NavController, + modifier: Modifier = Modifier, + isRoomCreator: Boolean = true, +) { + val maxPlayers by viewModel.maxPlayers.collectAsState() + val snippetDuration by viewModel.snippetDuration.collectAsState() + val pointsToWin by viewModel.pointsToWin.collectAsState() + val playlistLink by viewModel.playlistLink.collectAsState() + + Scaffold( + bottomBar = { + if (isRoomCreator) { + BottomNavigationBar(navController = navController) + } + }, + content = { padding -> + Column( + modifier = modifier.fillMaxSize().padding(padding).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + SettingSlider( + label = stringResource(R.string.number_of_players), + value = maxPlayers, + onValueChange = { viewModel.setMaxPlayers(it.toInt()) }, + valueRange = 1..5, + steps = 4, + ) + + SettingSlider( + label = stringResource(R.string.snippet_duration), + value = snippetDuration, + onValueChange = { viewModel.setSnippetDuration(it.toInt()) }, + valueRange = 5..30, + steps = 5, + ) + + SettingSlider( + label = stringResource(R.string.points_to_win), + value = pointsToWin, + onValueChange = { viewModel.setPointsToWin(it.toInt()) }, + valueRange = 3..10, + steps = 6, + ) + + Text( + text = stringResource(R.string.playlist_link), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp), + ) + + TextField( + value = playlistLink, + onValueChange = { viewModel.setPlaylistLink(it) }, + modifier = Modifier.fillMaxWidth().height(56.dp).padding(bottom = 16.dp), + label = { Text(stringResource(R.string.enter_playlist_link)) }, + ) + } + }, + ) +} + +@Composable +fun SettingSlider( + label: String, + value: Int, + onValueChange: (Int) -> Unit, + valueRange: IntRange, + steps: Int, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = label, style = MaterialTheme.typography.bodyMedium) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Slider( + value = value.toFloat(), + onValueChange = { onValueChange(it.toInt()) }, + valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(), + steps = steps - 1, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(16.dp)) + Text(text = value.toString(), style = MaterialTheme.typography.bodyMedium) + } + } +} + +@Composable +fun BottomNavigationBar(navController: NavController, modifier: Modifier = Modifier) { + NavigationBar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + ) { + NavigationBarItem( + icon = { + Icon(Icons.Filled.Home, contentDescription = stringResource(R.string.selected_room)) + }, + label = { Text(stringResource(R.string.selected_room)) }, + selected = false, + onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + ) + NavigationBarItem( + icon = { + Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) + }, + label = { Text(stringResource(R.string.settings)) }, + selected = false, + onClick = { navController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Composable +fun PreviewRoomSettingsScreen() { + val navController = rememberNavController() + RoomSettingsScreen(navController = navController) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsViewModel.kt new file mode 100644 index 0000000..0e2b276 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsViewModel.kt @@ -0,0 +1,46 @@ +package com.github.feelbeatapp.androidclient.ui.roomSettings + +import androidx.lifecycle.ViewModel +import com.github.feelbeatapp.androidclient.ui.home.Room +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class RoomSettingsViewModel : ViewModel() { + + private val _maxPlayers = MutableStateFlow(0) + val maxPlayers: StateFlow = _maxPlayers.asStateFlow() + + private val _snippetDuration = MutableStateFlow(0) + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() + + private val _pointsToWin = MutableStateFlow(0) + val pointsToWin: StateFlow = _pointsToWin.asStateFlow() + + private val _playlistLink = MutableStateFlow("") + val playlistLink: StateFlow + get() = _playlistLink.asStateFlow() + + fun loadRoomSettings(room: Room) { + _maxPlayers.value = room.maxPlayers + _snippetDuration.value = room.snippetDuration + _pointsToWin.value = room.pointsToWin + _playlistLink.value = room.playlistLink + } + + fun setMaxPlayers(value: Int) { + _maxPlayers.value = value + } + + fun setSnippetDuration(value: Int) { + _snippetDuration.value = value + } + + fun setPointsToWin(value: Int) { + _pointsToWin.value = value + } + + fun setPlaylistLink(value: String) { + _playlistLink.value = value + } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameScreen.kt new file mode 100644 index 0000000..55972db --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameScreen.kt @@ -0,0 +1,94 @@ +package com.github.feelbeatapp.androidclient.ui.startGame + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +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.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute + +@Composable +fun StartGameScreen( + viewModel: StartGameViewModel = StartGameViewModel(), + navController: NavController, +) { + val players by viewModel.players.collectAsState() + var countdown by remember { mutableIntStateOf(value = 3) } + + LaunchedEffect(key1 = countdown) { + if (countdown > 0) { + kotlinx.coroutines.delay(timeMillis = 1000) + countdown -= 1 + } else { + navController.navigate(FeelBeatRoute.GUESS_SONG.name) + } + } + + Column( + modifier = + Modifier.fillMaxSize().padding(16.dp).background(MaterialTheme.colorScheme.background), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { + players.forEach { player -> PlayerCard(player = player) } + } + Text( + text = countdown.toString(), + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier.padding(top = 32.dp), + ) + } +} + +@Composable +fun PlayerCard(player: Player) { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp)) { + Image( + painter = painterResource(id = player.image), + contentDescription = stringResource(R.string.player_image), + modifier = + Modifier.size(80.dp) + .clip(CircleShape) + .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape), + ) + Text( + text = player.name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(top = 10.dp), + ) + } +} + +@Preview +@Composable +fun StartGamePreview() { + val navController = rememberNavController() + StartGameScreen(navController = navController) +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameViewModel.kt new file mode 100644 index 0000000..3b2fcd1 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameViewModel.kt @@ -0,0 +1,32 @@ +package com.github.feelbeatapp.androidclient.ui.startGame + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.feelbeatapp.androidclient.R +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class Player(val name: String, val image: Int) + +class StartGameViewModel : ViewModel() { + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players.asStateFlow() + + init { + loadPlayers() + } + + private fun loadPlayers() { + viewModelScope.launch { + val examplePlayers = + listOf( + Player("User123", R.drawable.userimage), + Player("User456", R.drawable.userimage), + Player("User789", R.drawable.userimage), + ) + _players.value = examplePlayers + } + } +} diff --git a/app/src/main/res/drawable/userimage.webp b/app/src/main/res/drawable/userimage.webp new file mode 100644 index 0000000..10641c5 Binary files /dev/null and b/app/src/main/res/drawable/userimage.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67d932d..f06b46e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,9 +1,35 @@ FeelBeatAndroidClient Go back - Notifications + Settings Account Login with spotify - + Current games + Edit room + Feel Beat + Back + PLAY + Player Image + Snippet Duration: %1$d seconds + Points to Win: %1$d + Playlist: %1$s + New room + Number of players + Snippet Duration + Points to Win + Playlist link + Enter playlist link + Create room + Login screen + Guess the song + Player Avatar + Music Control + Search + Selected Room + Save Settings + Room Settings + You guessed song correctly! + Ups, that\'s not correct + time left Authorization server is unreachable \ No newline at end of file diff --git a/app/src/test/java/com/github/feelbeatapp/androidclient/GameViewModelTest.kt b/app/src/test/java/com/github/feelbeatapp/androidclient/GameViewModelTest.kt deleted file mode 100644 index 5aa2efe..0000000 --- a/app/src/test/java/com/github/feelbeatapp/androidclient/GameViewModelTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.feelbeatapp.androidclient - -import com.github.feelbeatapp.androidclient.rules.MainDispatcherRule -import com.github.feelbeatapp.androidclient.ui.game.GameViewModel -import com.github.feelbeatapp.androidclient.utils.FakeNetworkAgent -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test - -const val TEST_TEXT = "Very simple text" - -/** - * Example simple unit test - */ -@OptIn(ExperimentalCoroutinesApi::class) -class GameViewModelTest { - @get:Rule val mainDispatcherRule = MainDispatcherRule(StandardTestDispatcher()) - - @Test - fun gameViewModel_receiveMessage_updatesState() = runTest { - val fakeNetworkAgent = FakeNetworkAgent() - val vm = GameViewModel(fakeNetworkAgent) - - advanceUntilIdle() - fakeNetworkAgent.incoming.emit(TEST_TEXT) - - assertEquals(TEST_TEXT, vm.state.first().textInput) - } - - @Test - fun gameViewModel_onTextInput_sendMessage() = runTest { - val fakeNetworkAgent = FakeNetworkAgent() - val vm = GameViewModel(fakeNetworkAgent) - - advanceUntilIdle() - vm.setText(TEST_TEXT) - advanceUntilIdle() - - assertEquals(1, fakeNetworkAgent.sentMessages.size) - assertEquals(TEST_TEXT, fakeNetworkAgent.sentMessages.first()) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0124071..75c874b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" -composeBom = "2024.11.00" -navigationCompose = "2.8.4" +composeBom = "2024.12.01" +navigationCompose = "2.8.5" ktor = "3.0.1" hiltAndroid = "2.52" devtoolsKsp = "2.0.21-1.0.28"