From 32b10d3f47357be070b43a0d0d8c841b9c233ef5 Mon Sep 17 00:00:00 2001 From: vltkv <119106723+vltkv@users.noreply.github.com> Date: Sat, 7 Dec 2024 11:11:52 +0100 Subject: [PATCH 1/7] Basic screens --- .../androidclient/ui/FeelBeatApp.kt | 2 +- .../ui/guessSong/GuessSongScreen.kt | 183 +++++++++++++++ .../ui/guessSong/GuessSongViewModel.kt | 65 ++++++ .../ui/guessSong/ResultScreen.kt | 146 ++++++++++++ .../androidclient/ui/home/HomeScreen.kt | 210 ++++++++++-------- .../androidclient/ui/home/HomeViewModel.kt | 71 +++--- .../newRoomSettings/NewRoomSettingsScreen.kt | 162 ++++++++++++++ .../NewRoomSettingsViewModel.kt | 38 ++++ .../ui/startGame/StartGameScreen.kt | 112 ++++++++++ .../ui/startGame/StartGameViewModel.kt | 31 +++ app/src/main/res/drawable/userimage.png | Bin 0 -> 26296 bytes app/src/main/res/values/strings.xml | 2 +- 12 files changed, 899 insertions(+), 123 deletions(-) create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongViewModel.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsViewModel.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameViewModel.kt create mode 100644 app/src/main/res/drawable/userimage.png 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..7ba18c1 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 @@ -27,7 +27,7 @@ fun FeelBeatApp( composable(route = FeelBeatRoute.LOGIN.name) { LoginScreen() } composable(route = FeelBeatRoute.HOME.name) { - HomeScreen(parentNavController = navController) + //HomeScreen(parentNavController = navController) } composable(route = FeelBeatRoute.GAME.name) { GameScreen() } 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..65bf8f4 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongScreen.kt @@ -0,0 +1,183 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +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.material3.* +import androidx.compose.runtime.* +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.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import androidx.compose.ui.tooling.preview.Preview + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GuessSongScreen( + parentNavController: NavHostController, + viewModel: GuessSongViewModel = GuessSongViewModel() +) { + val players by viewModel.players.collectAsState() + val playlist by viewModel.playlist.collectAsState() + var searchQuery by remember { mutableStateOf(TextFieldValue("")) } + + Scaffold( + topBar = { TopAppBar(title = { Text("Playlist#1") }) } + ) { 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() + ) { + players.forEach { player -> + PlayerStatusIcon(player = player) + } + } + + MusicControlSlider() + + Column { + Text(text = "Guess the song", style = MaterialTheme.typography.bodyMedium) + SearchBar( + searchQuery = searchQuery, + onSearchQueryChange = { searchQuery = it } + ) + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxSize() + ) { + items(playlist.size) { index -> + SongItem( + song = playlist[index], + onClick = {/*TODO guess result*/} + ) + } + } + } + } +} + +@Composable +fun PlayerStatusIcon(player: Player1) { + val icon = when (player.status) { + // PlayerStatus.CORRECT -> Icons.Outlined.CheckCircle + //PlayerStatus.WRONG -> Icons.Outlined.Error + "BRAK" -> null + else -> null + } + val color = when (player.status) { +// PlayerStatus.CORRECT -> Color.Green +// PlayerStatus.WRONG -> Color.Red +// PlayerStatus.UNANSWERED -> Color.Gray + else -> null + } + + Box(modifier = Modifier.size(48.dp)) { + Image( + painter = painterResource(id = player.image), + contentDescription = "Player Avatar", + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + //.border(2.dp, color, CircleShape) + ) + icon?.let { + Icon( + imageVector = it, + contentDescription = null, + //tint = color, + modifier = Modifier + .align(Alignment.BottomEnd) + .size(16.dp) + ) + } + } +} + +@Composable +fun MusicControlSlider() { + Column { + Text("Music Control", style = MaterialTheme.typography.bodyMedium) + Slider( + value = 0.5f, + onValueChange = { /* 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 = "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.name, style = MaterialTheme.typography.bodyLarge) + } +// Image( +// painter = painterResource(id = song.image), +// contentDescription = null, +// modifier = Modifier.size(48.dp) +// ) + } + } +} + +@Preview +@Composable +fun GuessSongPreview() { + val nav = rememberNavController() + GuessSongScreen(parentNavController = 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..d1743e4 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessSongViewModel.kt @@ -0,0 +1,65 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.startGame.Player +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +//Zmienic status na enuma? +data class Player1(val name: String, val image: Int, val status: String) +data class Song(val id: Int, val name: String, val artist: String) +data class Result(val isCorrect: Boolean, val points: Int) + +class GuessSongViewModel : ViewModel() { + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players + + private val _playlist = MutableStateFlow>(emptyList()) + val playlist: StateFlow> = _playlist + + private val _currentSong = MutableStateFlow(null) + val currentSong: StateFlow = _currentSong + + private val _result = MutableStateFlow(null) + val result: StateFlow = _result + + init { + loadPlayers() + loadPlaylist() + } + + private fun loadPlayers() { + viewModelScope.launch { + val examplePlayers = listOf( + Player1("User123", R.drawable.userimage, "WRONG"), + Player1("User456", R.drawable.userimage, "CORRECT"), + Player1("User789", R.drawable.userimage, "WRONG") + ) + _players.value = examplePlayers + } + } + + private fun loadPlaylist() { + viewModelScope.launch { + val examplePlaylist = listOf( + Song(1, "Song 1", "artist1"), + Song(2, "Song 2", "artist1"), + Song(3, "Song 3", "artist1"), + Song(4, "Song 4", "artist1"), + Song(5, "Song 5", "artist1"), + Song(6, "Song 6", "artist1") + ) + _playlist.value = examplePlaylist + } + } + + fun submitAnswer(isCorrect: Boolean) { + viewModelScope.launch { + val points = if (isCorrect) 10 else 0 + _result.value = Result(isCorrect = isCorrect, points = points) + } + } +} diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt new file mode 100644 index 0000000..5572d5c --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt @@ -0,0 +1,146 @@ +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.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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R + +@Composable +fun ResultScreen( + parentNavController: NavHostController, + viewModel: GuessSongViewModel = GuessSongViewModel() +) { + val players by viewModel.players.collectAsState() + val currentSong by viewModel.currentSong.collectAsState() + val result by viewModel.result.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() + ) { + players.forEach { player -> + PlayerStatusIcon( + image = player.image, + isCorrect = (player.status == "CORRECT") + ) + } + } + currentSong?.let { song -> + Box( + modifier = Modifier + .padding(vertical = 16.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + SongInfo(songName = song.name, artistName = song.artist) + } + } + } + + result?.let { + Text( + text = if (it.isCorrect) "You guessed the song correctly!" else "You guessed wrong!", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + color = if (it.isCorrect) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 16.dp) + ) + } + + result?.let { + Text( + text = "Points: ${it.points}", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 32.dp) + ) + } + } +} + +@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(songName: String, artistName: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { +// Image( +// painter = painterResource(id = R.drawable.ic_playlist_image), // Replace with song image resource +// contentDescription = "Song Image", +// modifier = Modifier +// .size(50.dp) +// .clip(CircleShape) +// ) + Column { + Text( + text = songName, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = "By $artistName", + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@Preview +@Composable +fun ResultScreenPreview() { + val nav = rememberNavController() + ResultScreen(parentNavController = nav) +} 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..4eba85f 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.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.material.icons.Icons -import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.filled.Edit 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,103 @@ 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.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 +import androidx.compose.foundation.lazy.items + @Composable -fun HomeScreen( - parentNavController: NavHostController, - homeViewModel: HomeViewModel = hiltViewModel(), - modifier: Modifier = Modifier, -) { +fun HomeScreen(parentNavController: NavHostController, + viewModel: HomeViewModel = HomeViewModel(), + modifier: Modifier = Modifier) { 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 rooms by viewModel.rooms.collectAsState() + val selectedRoom by viewModel.selectedRoom.collectAsState() Scaffold(topBar = { HomeTopBar(title) }) { 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 = "Aktualne rozgrywki", + 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 = { viewModel.selectRoom(room) } + ) } } - HorizontalDivider() - - Button(onClick = { parentNavController.navigate(FeelBeatRoute.GAME.name) }) { - Text("Game") - } - - HorizontalDivider() - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().padding(0.dp, 20.dp), +// 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") +// } +// } +// +// Button(onClick = { parentNavController.navigate(FeelBeatRoute.GAME.name) }) { +// Text("Game") +// } + + 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), - ) + Button(onClick = { /*TODO new room settings*/}, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = (-15).dp, y = (-120).dp) + .size(60.dp), + ) { + //KUBA JA NIE WIEM CZEMU PLUSIK NIE JEST NA ŚRODKU, JAK SIĘ DA MINUSA TO JEST, RATUNKU :(((( + Text("+", style = MaterialTheme.typography.headlineMedium) + } + Button( + onClick = { /* TODO accept screen */ }, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(0.8f) + .height(60.dp), + enabled = selectedRoom != null + ) { + Text("NEXT", style = MaterialTheme.typography.headlineMedium) } } } @@ -133,16 +140,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 = {/*TODO account settings*/}) { Icon( imageVector = Icons.Outlined.Person, contentDescription = stringResource(R.string.account), @@ -152,11 +151,46 @@ 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) + ) + IconButton(onClick = { /* TODO Edit room */ }) { + Icon(Icons.Default.Edit, contentDescription = "Edit Room") + } + } + } +} + + @Preview @Composable fun HomePreview() { - FeelBeatTheme { - val nav = rememberNavController() - HomeScreen(nav) - } + val nav = rememberNavController() + HomeScreen(nav) } 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..673b94c 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,47 @@ 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}") - } +//TODO doac graczy, playliste, ustawienia +data class Room(val id: Int, val name: 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() + } + + private fun loadRooms() { + viewModelScope.launch { + val exampleRooms = listOf( + Room(1, "Pokoj 1"), + Room(2, "Pokoj 2"), + Room(3, "Pokoj 3"), + Room(4, "Pokoj 4"), + Room(5, "Pokoj 5"), + Room(6, "Pokoj 6"), + Room(7, "Pokoj 7") + ) + _rooms.value = exampleRooms } } -} + + fun selectRoom(room: Room) { + _selectedRoom.value = room + } + + fun addRoom(name: String) { + val newRoom = Room(id = _rooms.value.size + 1, name = name) + _rooms.value = _rooms.value + newRoom + } +} \ No newline at end of file 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..2e46a3a --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsScreen.kt @@ -0,0 +1,162 @@ +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.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.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + viewModel: NewRoomSettingsViewModel = NewRoomSettingsViewModel(), + modifier: Modifier = Modifier +) { + var maxPlayers by remember { mutableFloatStateOf(0f) } + var snippetDuration by remember { mutableFloatStateOf(0f) } + var pointsToWin by remember { mutableFloatStateOf(0f) } + var playlistLink by remember { mutableStateOf("") } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("New room") }, + navigationIcon = { + IconButton(onClick = { /* Handle back navigation */ }) { + Icon(Icons.Filled.KeyboardArrowLeft, contentDescription = "Back") + } + } + ) + }, + content = { padding -> + Column( + modifier = modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + SettingSlider( + label = "Number of players", + value = maxPlayers, + onValueChange = { maxPlayers = it }, + valueRange = 1f..5f, + steps = 4 + ) + + SettingSlider( + label = "Snippet duration", + value = snippetDuration, + onValueChange = { snippetDuration = it }, + valueRange = 5f..30f, + steps = 4 + ) + + SettingSlider( + label = "Points to win", + value = pointsToWin, + onValueChange = { pointsToWin = it }, + valueRange = 3f..10f, + steps = 6 + ) + + Text( + text = "Playlist link", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp) + ) + + TextField( + value = playlistLink, + onValueChange = { newValue -> + playlistLink = newValue + viewModel.setPlaylistLink(newValue) + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(bottom = 16.dp), + label = { Text("Enter playlist link") } + ) + + Button( + onClick = { /*TODO home screen*/ }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + ) { + Text("Create room") + } + } + } + ) +} + +@Composable +fun SettingSlider( + label: String, + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedRange, + 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, + onValueChange = onValueChange, + valueRange = valueRange as ClosedFloatingPointRange, + steps = steps, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = value.toInt().toString(), + style = MaterialTheme.typography.bodyMedium + ) + } + } +} + + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Composable +fun PreviewSettingsScreen() { + + SettingsScreen() +} \ No newline at end of file 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..82fc93f --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsViewModel.kt @@ -0,0 +1,38 @@ +package com.github.feelbeatapp.androidclient.ui.newRoomSettings + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class NewRoomSettingsViewModel : ViewModel() { + + private val _maxPlayers = MutableStateFlow(0) + val maxPlayers: StateFlow = _maxPlayers + + private val _snippetDuration = MutableStateFlow(0) + val snippetDuration: StateFlow = _snippetDuration + + private val _pointsToWin = MutableStateFlow(0) + val pointsToWin: StateFlow = _pointsToWin + + private val _playlistLink = MutableStateFlow("") + val playlistLink: StateFlow get() = _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..3e02565 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameScreen.kt @@ -0,0 +1,112 @@ +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.mutableStateOf +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController + + + +@Composable +fun StartGameScreen( + parentNavController: NavHostController, + viewModel: StartGameViewModel = StartGameViewModel(), + onCountdownFinish: () -> Unit +) { + val navController = rememberNavController() + val currentBackStack by navController.currentBackStackEntryAsState() + val title = currentBackStack?.destination?.route ?: "FeelBeat" + + val players by viewModel.players.collectAsState() + var countdown by remember { mutableStateOf(3) } + + LaunchedEffect(key1 = countdown) { + if (countdown > 0) { + kotlinx.coroutines.delay(1000) + countdown -= 1 + } else { + onCountdownFinish() + } + } + + 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, + //color = MaterialTheme.colorScheme.primary, + 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 = "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 nav = rememberNavController() + val onCountdownFinish = { println("Countdown finished!") } + StartGameScreen (nav, onCountdownFinish = onCountdownFinish) +} \ No newline at end of file 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..d79b999 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/startGame/StartGameViewModel.kt @@ -0,0 +1,31 @@ +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.png b/app/src/main/res/drawable/userimage.png new file mode 100644 index 0000000000000000000000000000000000000000..10641c541f43f7b948a03086dbb36f3c8f88d1d3 GIT binary patch literal 26296 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 z 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≤ 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%0AYW-B(hKmBDw_p&!Qj`@#y`^=J$zgh@=MB= zP(7kp>Z6v4Coaz+0=zwbB0e9}mNY=yP#>*leb`xCz5T#kITF{pO4OaX(ONw zfzz3aQ8B)9^_P#0jUB2`p;Gm~;p0Xe@t5x% z8(Vt)ZFW1jOP>R;>|i6ZRQ~Z5VuV~$+G{Ps=Og2a#y}f`Xx;u8&T?%xlo%g-gtWk# zg!yRM=%C*vq#n;X@VfWq^s!|QgP<|g)UpKcsTL+m#X^ja(AbHZnH^_6)-$P~OI-S~ zJ8=47l&5Lm2CbVgrJN<)<)CxN7Ubib?tKk2qs>+CCa50&&w{)@_B_e=!P|hk0L~J2 z@0KLU$45`rs)IR^D?=?&cU_gE`f{8;qN;}*8l|<&myx%G+Z`p$N4I!sCt64KO_&oH&^YFMkLyxHq$N1U+VZ# z`V;Q0M?y%W0AsL-y^({4?=^w&>=!yxfVZkUYu^*CGiUPSFLhj=LNZP=d%N9dxux1P z@UK4Nb;Rc!o-mvMZ$TlQd`BF8j;$l2)Kdf}$?*2*`S#zf&+aPHz0ckDpD$cS&h1N& zB*NRk^|fz{)-j0}6;b}YACN0mWwG4G!YD? z{n3eWpUUU#ehj#m|bj;G8rF`jqWS@98CE&E0E=vS| z{G+t*O|(wxmS4;gp=vzHz2!4SGJaNOP_D&=-6eFHkhRT{PhIVnVS>2Y`

l=1?w1(ppq^&@azvIm{0` z?(Q!otXuompKlIXnMAn)*PLG1w}B3`?EPJ4Mvt@SKPyZWmmL2551K@NreXbpo=G1h zZd<@3t`?lboQMq06D96TZMBD;X9TB-VN@*-c^IPKZ(*B=ZuP^BlhsG3%FHwAg4d9+ zH*A-aBEY-iD3>}K62i= z82nl$wAb_85A@3aKTtK78VwUOjFkG()<_^=#yQNZzGfCjjlTQB5Tm6p{{z;n-$|tm z%C4Su>I2Jo%RmYxs`t=oTzHp|Z|nK7QVY}KSGCdsXU}RQ{C3G#Dsxk>qJJ>CruX;t zA&ovy3)N|0IQaCjTYe0oS{8Yc$KH?G`2n@EOjBN@-;%|5K5hL>UH+{f^6pnAjB?`Q zTBn)e@O$XtHjT<0Yamx%re)fFdoQs5rI)(!hhg098W7<_!r_GgUjnQEbvQJx%VY8%udVguli7}(^QZ$n0(fOa+xcK zE%^OE`vILMiMO*bCq%@XC4LhPe?@1sS)wt_OH2BGYx?^8}cS6glcWZXmiH< zaP?1LCzn}J;mB^OeAC^ue92y6y40NL_JJ&+@bHHC34h3uluqK{=olNB2FxEC0g%UyTGo4+L(Zk zzTP*|673tt8`xyjrs?)yf3C3xs^Wj@e_?Ozdbd2jabs^oTkAG%+_-Sw?dn{1 FeelBeatAndroidClient Go back - Notifications + Settings Account Login with spotify From 4868923ac6aefbd05dff273a530063d972ef52a0 Mon Sep 17 00:00:00 2001 From: vltkv <119106723+vltkv@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:33:15 +0100 Subject: [PATCH 2/7] navigation sketch --- .../androidclient/ui/FeelBeatApp.kt | 6 +- .../ui/acceptGame/AcceptGameScreen.kt | 131 ++++++++++++++++++ .../ui/acceptGame/AcceptGameViewModel.kt | 66 +++++++++ .../ui/gameResult/GameResultScreen.kt | 2 + .../ui/gameResult/GameResultViewModel.kt | 2 + .../androidclient/ui/home/HomeRoute.kt | 5 +- .../androidclient/ui/home/HomeScreen.kt | 47 +++---- .../newRoomSettings/NewRoomSettingsRoute.kt | 6 + .../newRoomSettings/NewRoomSettingsScreen.kt | 7 +- .../androidclient/GameViewModelTest.kt | 47 ------- 10 files changed, 239 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameViewModel.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultViewModel.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt delete mode 100644 app/src/test/java/com/github/feelbeatapp/androidclient/GameViewModelTest.kt 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 7ba18c1..90ac868 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,7 +8,7 @@ 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.game.GameScreen import com.github.feelbeatapp.androidclient.ui.home.HomeScreen import com.github.feelbeatapp.androidclient.ui.login.LoginScreen import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @@ -27,10 +27,10 @@ fun FeelBeatApp( composable(route = FeelBeatRoute.LOGIN.name) { LoginScreen() } composable(route = FeelBeatRoute.HOME.name) { - //HomeScreen(parentNavController = navController) + HomeScreen(parentNavController = navController) } - composable(route = FeelBeatRoute.GAME.name) { GameScreen() } + //composable(route = FeelBeatRoute.GAME.name) { GameScreen() } } } } 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..4882da7 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameScreen.kt @@ -0,0 +1,131 @@ +package com.github.feelbeatapp.androidclient.ui.acceptGame + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.border +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.painterResource +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import androidx.compose.ui.tooling.preview.Preview +import com.github.feelbeatapp.androidclient.ui.startGame.Player +import com.github.feelbeatapp.androidclient.ui.startGame.PlayerCard + +@Composable +fun AcceptScreen( + parentNavController: NavHostController, + viewModel: AcceptViewModel = AcceptViewModel(), +) { + val players = viewModel.players.collectAsState().value + val playlist = viewModel.playlist.collectAsState().value + val snippetDuration = viewModel.snippetDuration.collectAsState().value + val pointsToWin = viewModel.pointsToWin.collectAsState().value + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconButton(onClick = { /* TODO home */ }) { + Icon(Icons.Filled.KeyboardArrowLeft, contentDescription = "Back") + } + + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth() + ) { + players.forEach { player -> + PlayerCard(player = player) + } + } + + + Spacer(modifier = Modifier.height(32.dp)) + + Column( + horizontalAlignment = Alignment.Start + ) { + Text("Snippet Duration: $snippetDuration seconds", style = MaterialTheme.typography.bodyMedium) + Text("Points to Win: $pointsToWin", style = MaterialTheme.typography.bodyMedium) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text(text = "Playlist: ${playlist.name}", style = MaterialTheme.typography.titleLarge) + + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + ) { + playlist.songs.forEach { song -> + SongItem(song = song) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { /* TODO startgame */ }, + modifier = Modifier.fillMaxWidth(), + shape = CircleShape + ) { + Text("PLAY", style = MaterialTheme.typography.headlineMedium) + } + } +} + +@Composable +fun PlayerCard(player: Player) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + Image( + painter = painterResource(id = player.image), + contentDescription = "Player Image", + modifier = Modifier + .size(20.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) + ) + } +} + +@Composable +fun SongItem(song: Song) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = song.title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + } +} + +@Preview +@Composable +fun PreviewAcceptScreen() { + val navController = rememberNavController() + AcceptScreen(parentNavController = 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..80e5f81 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameViewModel.kt @@ -0,0 +1,66 @@ +package com.github.feelbeatapp.androidclient.ui.acceptGame + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +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.launch + +data class Song(val id: Int, val title: String) +data class Player(val name: String, val image: Int) +data class Playlist(val name: String, val songs: List) + +class AcceptViewModel : ViewModel() { + private val _songs = MutableStateFlow>(emptyList()) + val rooms: StateFlow> = _songs.asStateFlow() + + private val _selectedRoom= MutableStateFlow(null) + val selectedRoom: StateFlow = _selectedRoom.asStateFlow() + + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players.asStateFlow() + + private val _playlist = MutableStateFlow(Playlist(name = "Playlist #1", songs = emptyList())) + val playlist: StateFlow = _playlist.asStateFlow() + + private val _snippetDuration = MutableStateFlow(30) + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() + + private val _pointsToWin = MutableStateFlow(100) + val pointsToWin: StateFlow = _pointsToWin.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) + ) + _players.value = examplePlayers + } + } + + 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"), + ) + _songs.value = exampleSongs + } + } +} 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..7d14314 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultScreen.kt @@ -0,0 +1,2 @@ +package com.github.feelbeatapp.androidclient.ui.gameResult + 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..7d14314 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultViewModel.kt @@ -0,0 +1,2 @@ +package com.github.feelbeatapp.androidclient.ui.gameResult + 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 index 25d6ce9..16886c0 100644 --- 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 @@ -2,6 +2,7 @@ package com.github.feelbeatapp.androidclient.ui.home enum class HomeRoute { HOME, - CHOOSE_PLAYLIST, - GAME_SETTINGS, + ACCEPT_SCREEN, + ACCOUNT_SETTINGS, + NEW_ROOM_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 4eba85f..3e26803 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 @@ -39,6 +39,12 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.github.feelbeatapp.androidclient.R import androidx.compose.foundation.lazy.items +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.github.feelbeatapp.androidclient.ui.newRoomSettings.PreviewSettingsScreen +import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.ui.acceptGame.AcceptScreen +import com.github.feelbeatapp.androidclient.ui.newRoomSettings.SettingsScreen @Composable @@ -47,12 +53,13 @@ fun HomeScreen(parentNavController: NavHostController, modifier: Modifier = Modifier) { val navController = rememberNavController() val currentBackStack by navController.currentBackStackEntryAsState() - val title = currentBackStack?.destination?.route ?: "FeelBeat" + val title = "FeelBeat" val rooms by viewModel.rooms.collectAsState() val selectedRoom by viewModel.selectedRoom.collectAsState() Scaffold(topBar = { HomeTopBar(title) }) { innerPadding -> + Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { Text( text = "Aktualne rozgrywki", @@ -75,38 +82,26 @@ fun HomeScreen(parentNavController: NavHostController, } } -// 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") -// } -// } + NavHost(navController, startDestination = HomeRoute.HOME.name) { + composable(route = HomeRoute.HOME.name) { Text("Here list of games") } -// 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") + composable(route = HomeRoute.ACCEPT_SCREEN.name) { + AcceptScreen(parentNavController = navController) + } +// composable(route = HomeRoute.ACCOUNT_SETTINGS.name) { +// Text("Here choose game settings") // } -// } -// -// Button(onClick = { parentNavController.navigate(FeelBeatRoute.GAME.name) }) { -// Text("Game") -// } + composable(route = HomeRoute.NEW_ROOM_SETTINGS.name) { + SettingsScreen(parentNavController = navController) + } + } Box( modifier = Modifier .fillMaxWidth() .padding(bottom = 80.dp, start = 16.dp, end = 16.dp) ) { - Button(onClick = { /*TODO new room settings*/}, + Button(onClick = { navController.navigate(HomeRoute.NEW_ROOM_SETTINGS.name)}, modifier = Modifier .align(Alignment.BottomEnd) .offset(x = (-15).dp, y = (-120).dp) @@ -116,7 +111,7 @@ fun HomeScreen(parentNavController: NavHostController, Text("+", style = MaterialTheme.typography.headlineMedium) } Button( - onClick = { /* TODO accept screen */ }, + onClick = { navController.navigate(HomeRoute.ACCEPT_SCREEN.name)}, modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth(0.8f) diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt new file mode 100644 index 0000000..dff1ee8 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt @@ -0,0 +1,6 @@ +package com.github.feelbeatapp.androidclient.ui.newRoomSettings + +enum class NewRoomSettingsRoute { + NEW_ROOM_SETTINGS, + HOME, +} \ No newline at end of file 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 index 2e46a3a..e2ba8b8 100644 --- 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 @@ -31,10 +31,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( + parentNavController: NavHostController, viewModel: NewRoomSettingsViewModel = NewRoomSettingsViewModel(), modifier: Modifier = Modifier ) { @@ -157,6 +160,6 @@ fun SettingSlider( @Preview(showBackground = true, widthDp = 360, heightDp = 640) @Composable fun PreviewSettingsScreen() { - - SettingsScreen() + val navController = rememberNavController() + SettingsScreen(parentNavController = navController) } \ 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()) - } -} From dd4867ed1a31e403f0ee365714465d4c29cae8f5 Mon Sep 17 00:00:00 2001 From: vltkv <119106723+vltkv@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:55:26 +0100 Subject: [PATCH 3/7] add some navigation --- .../androidclient/ui/FeelBeatApp.kt | 51 ++-- .../androidclient/ui/FeelBeatRoute.kt | 13 +- .../ui/acceptGame/AcceptGameScreen.kt | 175 +++++++------ .../ui/acceptGame/AcceptGameViewModel.kt | 3 + .../ui/guessSong/GuessResultScreen.kt | 126 ++++++++++ .../ui/guessSong/GuessSongScreen.kt | 233 ++++++++---------- .../ui/guessSong/GuessSongViewModel.kt | 18 +- .../ui/guessSong/ResultScreen.kt | 146 ----------- .../ui/guessSong/ResultStatus.kt | 5 + .../androidclient/ui/home/HomeRoute.kt | 8 - .../androidclient/ui/home/HomeScreen.kt | 225 +++++++---------- .../androidclient/ui/login/LoginScreen.kt | 15 +- .../newRoomSettings/NewRoomSettingsRoute.kt | 6 - .../newRoomSettings/NewRoomSettingsScreen.kt | 84 +++---- .../NewRoomSettingsViewModel.kt | 9 +- .../ui/startGame/StartGameScreen.kt | 102 +++----- .../ui/startGame/StartGameViewModel.kt | 28 +-- app/src/main/res/values/strings.xml | 22 +- 18 files changed, 602 insertions(+), 667 deletions(-) create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessResultScreen.kt delete mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultStatus.kt delete mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeRoute.kt delete mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt 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 90ac868..59179a4 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,36 +8,57 @@ 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.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.startGame.StartGameScreen import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable fun FeelBeatApp( @Suppress("UnusedParameter") widthSizeClass: WindowWidthSizeClass, - startRoute: FeelBeatRoute, modifier: Modifier = Modifier, ) { - FeelBeatTheme { - val navController = rememberNavController() + FeelBeatTheme { + val navController = rememberNavController() - Box(modifier = modifier) { - NavHost(navController, startDestination = startRoute.name) { - composable(route = FeelBeatRoute.LOGIN.name) { LoginScreen() } - - composable(route = FeelBeatRoute.HOME.name) { - HomeScreen(parentNavController = navController) - } - - //composable(route = FeelBeatRoute.GAME.name) { GameScreen() } - } + Box(modifier = modifier) { + NavHost(navController, startDestination = FeelBeatRoute.LOGIN.name) { + composable(route = FeelBeatRoute.LOGIN.name) { + LoginScreen(onLoggedIn = { navController.navigate(FeelBeatRoute.HOME.name) }) + } + composable(route = FeelBeatRoute.HOME.name) { HomeScreen(navController = navController) } + composable(route = FeelBeatRoute.NEW_ROOM_SETTINGS.name) { + NewRoomSettingsScreen(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) } + } } + } } @Preview @Composable fun AppPreview() { - FeelBeatApp(WindowWidthSizeClass.Compact, FeelBeatRoute.HOME) + FeelBeatApp(WindowWidthSizeClass.Compact) } 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..4592e8e 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 @@ -1,8 +1,13 @@ package com.github.feelbeatapp.androidclient.ui enum class FeelBeatRoute { - LOGIN, - HOME, - LOBBY, - GAME, + LOGIN, + HOME, + NEW_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/acceptGame/AcceptGameScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/acceptGame/AcceptGameScreen.kt index 4882da7..1fe3801 100644 --- 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 @@ -1,131 +1,130 @@ package com.github.feelbeatapp.androidclient.ui.acceptGame import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.verticalScroll -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowLeft -import androidx.compose.material3.* +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp import androidx.compose.ui.res.painterResource -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController +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.Player import com.github.feelbeatapp.androidclient.ui.startGame.PlayerCard @Composable -fun AcceptScreen( - parentNavController: NavHostController, - viewModel: AcceptViewModel = AcceptViewModel(), -) { - val players = viewModel.players.collectAsState().value - val playlist = viewModel.playlist.collectAsState().value - val snippetDuration = viewModel.snippetDuration.collectAsState().value - val pointsToWin = viewModel.pointsToWin.collectAsState().value +fun AcceptGameScreen(viewModel: AcceptViewModel = AcceptViewModel(), navController: NavController) { + val players = viewModel.players.collectAsState().value + val playlist = viewModel.playlist.collectAsState().value + val snippetDuration = viewModel.snippetDuration.collectAsState().value + val pointsToWin = viewModel.pointsToWin.collectAsState().value - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - IconButton(onClick = { /* TODO home */ }) { - Icon(Icons.Filled.KeyboardArrowLeft, contentDescription = "Back") - } + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = stringResource(R.string.back)) + } - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier.fillMaxWidth() - ) { - players.forEach { player -> - PlayerCard(player = player) - } - } + Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { + players.forEach { player -> PlayerCard(player = player) } + } + Spacer(modifier = Modifier.height(32.dp)) - Spacer(modifier = Modifier.height(32.dp)) + Column(horizontalAlignment = Alignment.Start) { + Text( + text = stringResource(id = R.string.snippet_duration_val, snippetDuration), + style = MaterialTheme.typography.bodyMedium) + Text( + text = stringResource(id = R.string.points_to_win_val, pointsToWin), + style = MaterialTheme.typography.bodyMedium) + } - Column( - horizontalAlignment = Alignment.Start - ) { - Text("Snippet Duration: $snippetDuration seconds", style = MaterialTheme.typography.bodyMedium) - Text("Points to Win: $pointsToWin", style = MaterialTheme.typography.bodyMedium) - } + Spacer(modifier = Modifier.height(32.dp)) - Spacer(modifier = Modifier.height(32.dp)) + Text( + text = stringResource(id = R.string.playlist_name, playlist.name), + style = MaterialTheme.typography.titleLarge) - Text(text = "Playlist: ${playlist.name}", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(8.dp)) + Column(modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxWidth()) { + playlist.songs.forEach { song -> SongItem(song = song) } + } - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .fillMaxWidth() - ) { - playlist.songs.forEach { song -> - SongItem(song = song) - } - } - - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(32.dp)) - Button( - onClick = { /* TODO startgame */ }, - modifier = Modifier.fillMaxWidth(), - shape = CircleShape - ) { - Text("PLAY", style = MaterialTheme.typography.headlineMedium) + Button( + onClick = { navController.navigate(FeelBeatRoute.START_GAME.name) }, + modifier = Modifier.fillMaxWidth(), + shape = CircleShape) { + Text(stringResource(R.string.play), style = MaterialTheme.typography.headlineMedium) } - } + } } @Composable fun PlayerCard(player: Player) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(16.dp) - ) { - Image( - painter = painterResource(id = player.image), - contentDescription = "Player Image", - modifier = Modifier - .size(20.dp) + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp)) { + Image( + painter = painterResource(id = player.image), + contentDescription = stringResource(R.string.player_image), + modifier = + Modifier.size(20.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) - ) - } + .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)) + } } @Composable fun SongItem(song: Song) { - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = song.title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) - } + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically) { + Text( + text = song.title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f)) + } } @Preview @Composable fun PreviewAcceptScreen() { - val navController = rememberNavController() - AcceptScreen(parentNavController = navController) + 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 index 80e5f81..e8fa248 100644 --- 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 @@ -16,6 +16,8 @@ data class Song(val id: Int, val title: String) data class Player(val name: String, val image: Int) data class Playlist(val name: String, val songs: List) +//klasa mająca wszystkie stany -> jedno mutable state flow + class AcceptViewModel : ViewModel() { private val _songs = MutableStateFlow>(emptyList()) val rooms: StateFlow> = _songs.asStateFlow() @@ -48,6 +50,7 @@ class AcceptViewModel : ViewModel() { Player("User789", R.drawable.userimage) ) _players.value = examplePlayers + //update zamiast value } } 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..ebb32a6 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/GuessResultScreen.kt @@ -0,0 +1,126 @@ +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.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.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 + +@Composable +fun GuessResultScreen( + navController: NavController, + viewModel: GuessSongViewModel = GuessSongViewModel() +) { + val players by viewModel.players.collectAsState() + val currentSong by viewModel.currentSong.collectAsState() + val result by viewModel.result.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()) { + players.forEach { player -> + PlayerStatusIcon( + image = player.image, isCorrect = (player.status == ResultStatus.CORRECT)) + } + } + currentSong?.let { song -> + Box( + modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth(), + contentAlignment = Alignment.Center) { + SongInfo(songName = song.name, artistName = song.artist) + } + } + } + + result?.let { + Text( + text = if (it.isCorrect) "You guessed the song correctly!" else "You guessed wrong!", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + color = + if (it.isCorrect) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 16.dp)) + } + + result?.let { + Text( + text = "Points: ${it.points}", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 32.dp)) + } + } +} + +@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(songName: String, artistName: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp)) { + // Image( + // painter = painterResource(id = R.drawable.ic_playlist_image), // Replace with + // song image resource + // contentDescription = "Song Image", + // modifier = Modifier + // .size(50.dp) + // .clip(CircleShape) + // ) + Column { + Text( + text = songName, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold) + Text(text = "By $artistName", style = MaterialTheme.typography.bodySmall) + } + } +} + +@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 index 65bf8f4..c1b136f 100644 --- 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 @@ -2,182 +2,167 @@ package com.github.feelbeatapp.androidclient.ui.guessSong import androidx.compose.foundation.Image import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +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.material3.* -import androidx.compose.runtime.* +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.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController +import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import androidx.compose.ui.tooling.preview.Preview +import com.github.feelbeatapp.androidclient.R +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute @OptIn(ExperimentalMaterial3Api::class) @Composable fun GuessSongScreen( - parentNavController: NavHostController, + navController: NavController, viewModel: GuessSongViewModel = GuessSongViewModel() ) { - val players by viewModel.players.collectAsState() - val playlist by viewModel.playlist.collectAsState() - var searchQuery by remember { mutableStateOf(TextFieldValue("")) } + val players by viewModel.players.collectAsState() + val playlist by viewModel.playlist.collectAsState() + val searchQuery by viewModel.searchQuery.collectAsState() - Scaffold( - topBar = { TopAppBar(title = { Text("Playlist#1") }) } - ) { 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() - ) { - players.forEach { player -> - PlayerStatusIcon(player = player) - } - } + Scaffold(topBar = { TopAppBar(title = { Text("Playlist#1") }) }) { 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()) { + players.forEach { player -> PlayerStatusIcon(player = player) } + } - MusicControlSlider() + MusicControlSlider() - Column { - Text(text = "Guess the song", style = MaterialTheme.typography.bodyMedium) - SearchBar( - searchQuery = searchQuery, - onSearchQueryChange = { searchQuery = it } - ) - } + Column { + Text( + text = stringResource(R.string.guess_the_song), + style = MaterialTheme.typography.bodyMedium) + SearchBar( + searchQuery = searchQuery, + onSearchQueryChange = { viewModel.updateSearchQuery(it) }) + } - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxSize() - ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize()) { items(playlist.size) { index -> - SongItem( - song = playlist[index], - onClick = {/*TODO guess result*/} - ) + SongItem( + song = playlist[index], + onClick = { navController.navigate(FeelBeatRoute.GUESS_RESULT.name) }) } - } + } } - } + } } @Composable fun PlayerStatusIcon(player: Player1) { - val icon = when (player.status) { - // PlayerStatus.CORRECT -> Icons.Outlined.CheckCircle - //PlayerStatus.WRONG -> Icons.Outlined.Error - "BRAK" -> null + val icon = + when (player.status) { + // PlayerStatus.CORRECT -> Icons.Outlined.CheckCircle + // PlayerStatus.WRONG -> Icons.Outlined.Error + ResultStatus.NORESPONSE -> null else -> null - } - val color = when (player.status) { -// PlayerStatus.CORRECT -> Color.Green -// PlayerStatus.WRONG -> Color.Red -// PlayerStatus.UNANSWERED -> Color.Gray + } + val color = + when (player.status) { + // PlayerStatus.CORRECT -> Color.Green + // PlayerStatus.WRONG -> Color.Red + // PlayerStatus.UNANSWERED -> Color.Gray else -> null - } + } - Box(modifier = Modifier.size(48.dp)) { - Image( - painter = painterResource(id = player.image), - contentDescription = "Player Avatar", - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - //.border(2.dp, color, CircleShape) - ) - icon?.let { - Icon( - imageVector = it, - contentDescription = null, - //tint = color, - modifier = Modifier - .align(Alignment.BottomEnd) - .size(16.dp) - ) - } + Box(modifier = Modifier.size(48.dp)) { + Image( + painter = painterResource(id = player.image), + contentDescription = stringResource(R.string.player_avatar), + modifier = Modifier.size(48.dp).clip(CircleShape)) + icon?.let { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier.align(Alignment.BottomEnd).size(16.dp)) } + } } @Composable fun MusicControlSlider() { - Column { - Text("Music Control", style = MaterialTheme.typography.bodyMedium) - Slider( - value = 0.5f, - onValueChange = { /* Handle slider change */ }, - modifier = Modifier.fillMaxWidth() - ) - } + 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 - ) { +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 = "Search") - } + 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.name, style = MaterialTheme.typography.bodyLarge) - } -// Image( -// painter = painterResource(id = song.image), -// contentDescription = null, -// modifier = Modifier.size(48.dp) -// ) +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.name, style = MaterialTheme.typography.bodyLarge) } + // Image( + // painter = painterResource(id = song.image), + // contentDescription = null, + // modifier = Modifier.size(48.dp) + // ) } - } + } } @Preview @Composable fun GuessSongPreview() { - val nav = rememberNavController() - GuessSongScreen(parentNavController = nav) + 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 index d1743e4..c103fa9 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -8,8 +9,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -//Zmienic status na enuma? -data class Player1(val name: String, val image: Int, val status: String) +// dodatkowa klasa na wszystko, +data class Player1(val name: String, val image: Int, val status: ResultStatus) data class Song(val id: Int, val name: String, val artist: String) data class Result(val isCorrect: Boolean, val points: Int) @@ -20,6 +21,9 @@ class GuessSongViewModel : ViewModel() { private val _playlist = MutableStateFlow>(emptyList()) val playlist: StateFlow> = _playlist + private val _searchQuery = MutableStateFlow(TextFieldValue("")) + val searchQuery: StateFlow = _searchQuery + private val _currentSong = MutableStateFlow(null) val currentSong: StateFlow = _currentSong @@ -34,9 +38,9 @@ class GuessSongViewModel : ViewModel() { private fun loadPlayers() { viewModelScope.launch { val examplePlayers = listOf( - Player1("User123", R.drawable.userimage, "WRONG"), - Player1("User456", R.drawable.userimage, "CORRECT"), - Player1("User789", R.drawable.userimage, "WRONG") + Player1("User123", R.drawable.userimage, ResultStatus.CORRECT), + Player1("User456", R.drawable.userimage, ResultStatus.WRONG), + Player1("User789", R.drawable.userimage, ResultStatus.NORESPONSE) ) _players.value = examplePlayers } @@ -56,6 +60,10 @@ class GuessSongViewModel : ViewModel() { } } + fun updateSearchQuery(newQuery: TextFieldValue) { + _searchQuery.value = newQuery + } + fun submitAnswer(isCorrect: Boolean) { viewModelScope.launch { val points = if (isCorrect) 10 else 0 diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt deleted file mode 100644 index 5572d5c..0000000 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultScreen.kt +++ /dev/null @@ -1,146 +0,0 @@ -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.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.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.github.feelbeatapp.androidclient.R - -@Composable -fun ResultScreen( - parentNavController: NavHostController, - viewModel: GuessSongViewModel = GuessSongViewModel() -) { - val players by viewModel.players.collectAsState() - val currentSong by viewModel.currentSong.collectAsState() - val result by viewModel.result.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() - ) { - players.forEach { player -> - PlayerStatusIcon( - image = player.image, - isCorrect = (player.status == "CORRECT") - ) - } - } - currentSong?.let { song -> - Box( - modifier = Modifier - .padding(vertical = 16.dp) - .fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - SongInfo(songName = song.name, artistName = song.artist) - } - } - } - - result?.let { - Text( - text = if (it.isCorrect) "You guessed the song correctly!" else "You guessed wrong!", - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - color = if (it.isCorrect) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, - modifier = Modifier.padding(top = 16.dp) - ) - } - - result?.let { - Text( - text = "Points: ${it.points}", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 32.dp) - ) - } - } -} - -@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(songName: String, artistName: String) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { -// Image( -// painter = painterResource(id = R.drawable.ic_playlist_image), // Replace with song image resource -// contentDescription = "Song Image", -// modifier = Modifier -// .size(50.dp) -// .clip(CircleShape) -// ) - Column { - Text( - text = songName, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold - ) - Text( - text = "By $artistName", - style = MaterialTheme.typography.bodySmall - ) - } - } -} - -@Preview -@Composable -fun ResultScreenPreview() { - val nav = rememberNavController() - ResultScreen(parentNavController = nav) -} 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..de8369d --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/guessSong/ResultStatus.kt @@ -0,0 +1,5 @@ +package com.github.feelbeatapp.androidclient.ui.guessSong + +enum class ResultStatus { + CORRECT, WRONG, NORESPONSE +} \ No newline at end of file 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 16886c0..0000000 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeRoute.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.feelbeatapp.androidclient.ui.home - -enum class HomeRoute { - HOME, - ACCEPT_SCREEN, - ACCOUNT_SETTINGS, - NEW_ROOM_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 3e26803..c99b14b 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,5 +1,6 @@ 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 @@ -7,15 +8,14 @@ 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.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit +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 @@ -34,158 +34,113 @@ 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.NavHostController -import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.github.feelbeatapp.androidclient.R -import androidx.compose.foundation.lazy.items -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import com.github.feelbeatapp.androidclient.ui.newRoomSettings.PreviewSettingsScreen -import androidx.navigation.compose.rememberNavController -import com.github.feelbeatapp.androidclient.ui.acceptGame.AcceptScreen -import com.github.feelbeatapp.androidclient.ui.newRoomSettings.SettingsScreen - +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute @Composable -fun HomeScreen(parentNavController: NavHostController, - viewModel: HomeViewModel = HomeViewModel(), - modifier: Modifier = Modifier) { - val navController = rememberNavController() - val currentBackStack by navController.currentBackStackEntryAsState() - val title = "FeelBeat" - - val rooms by viewModel.rooms.collectAsState() - val selectedRoom by viewModel.selectedRoom.collectAsState() - - Scaffold(topBar = { HomeTopBar(title) }) { innerPadding -> - - Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { - Text( - text = "Aktualne rozgrywki", - 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 = { viewModel.selectRoom(room) } - ) - } - } - - NavHost(navController, startDestination = HomeRoute.HOME.name) { - composable(route = HomeRoute.HOME.name) { Text("Here list of games") } +fun HomeScreen( + viewModel: HomeViewModel = HomeViewModel(), + modifier: Modifier = Modifier, + navController: NavController, +) { + val title = stringResource(R.string.feel_beat) + val rooms by viewModel.rooms.collectAsState() + val selectedRoom by viewModel.selectedRoom.collectAsState() - composable(route = HomeRoute.ACCEPT_SCREEN.name) { - AcceptScreen(parentNavController = navController) - } -// composable(route = HomeRoute.ACCOUNT_SETTINGS.name) { -// Text("Here choose game settings") -// } - composable(route = HomeRoute.NEW_ROOM_SETTINGS.name) { - SettingsScreen(parentNavController = navController) - } + Scaffold(topBar = { HomeTopBar(title, navController) }) { innerPadding -> + Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { + 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) }) } + } - Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 80.dp, start = 16.dp, end = 16.dp) - ) { - Button(onClick = { navController.navigate(HomeRoute.NEW_ROOM_SETTINGS.name)}, - modifier = Modifier - .align(Alignment.BottomEnd) - .offset(x = (-15).dp, y = (-120).dp) - .size(60.dp), - ) { - //KUBA JA NIE WIEM CZEMU PLUSIK NIE JEST NA ŚRODKU, JAK SIĘ DA MINUSA TO JEST, RATUNKU :(((( - Text("+", style = MaterialTheme.typography.headlineMedium) - } - Button( - onClick = { navController.navigate(HomeRoute.ACCEPT_SCREEN.name)}, - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(0.8f) - .height(60.dp), - enabled = selectedRoom != null - ) { - Text("NEXT", style = MaterialTheme.typography.headlineMedium) - } + Box(modifier = Modifier.fillMaxWidth().padding(bottom = 80.dp, start = 16.dp, end = 16.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) + } } - } + // TODO wywalic next -> po kliknieciu na playliste -> accept screen, w którym admin może + // jeszcze raz edytowac ustawieina, accept screen pasek na dole albo widok pokoju albo + // ustawienia? + } } + } } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeTopBar(title: String) { - CenterAlignedTopAppBar( - title = { Text(title) }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary, - ), - actions = { - IconButton(onClick = {/*TODO account settings*/}) { - Icon( - imageVector = Icons.Outlined.Person, - contentDescription = stringResource(R.string.account), - ) - } - }, - ) +fun HomeTopBar(title: String, navController: NavController) { + CenterAlignedTopAppBar( + title = { Text(title) }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + actions = { + IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCOUNT_SETTINGS.name) }) { + Icon( + imageVector = Icons.Outlined.Person, + contentDescription = stringResource(R.string.account), + ) + } + }, + ) } @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) - ) - IconButton(onClick = { /* TODO Edit room */ }) { - Icon(Icons.Default.Edit, contentDescription = "Edit Room") - } +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() { - val nav = rememberNavController() - HomeScreen(nav) + val navController = rememberNavController() + HomeScreen(navController = navController) } 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..e174d41 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 @@ -8,26 +8,17 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.github.feelbeatapp.androidclient.R @Composable -fun LoginScreen( - modifier: Modifier = Modifier, - loginViewModel: LoginViewModel = hiltViewModel(), -) { - val ctx = LocalContext.current - +fun LoginScreen(onLoggedIn: () -> Unit, modifier: Modifier = Modifier) { Column( verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.fillMaxSize(), ) { - Text("Login screen") - Button(onClick = { loginViewModel.login(ctx) }) { - Text(stringResource(R.string.login_with_spotify)) - } + Text(stringResource(R.string.login_screen)) + Button(onClick = onLoggedIn) { Text(stringResource(R.string.login_with_spotify)) } } } diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt deleted file mode 100644 index dff1ee8..0000000 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/newRoomSettings/NewRoomSettingsRoute.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.feelbeatapp.androidclient.ui.newRoomSettings - -enum class NewRoomSettingsRoute { - NEW_ROOM_SETTINGS, - HOME, -} \ No newline at end of file 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 index e2ba8b8..6565382 100644 --- 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 @@ -10,7 +10,7 @@ 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.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -22,37 +22,39 @@ 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.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue 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.NavHostController +import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import com.github.feelbeatapp.androidclient.R @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsScreen( - parentNavController: NavHostController, +fun NewRoomSettingsScreen( viewModel: NewRoomSettingsViewModel = NewRoomSettingsViewModel(), + navController: NavController, modifier: Modifier = Modifier ) { - var maxPlayers by remember { mutableFloatStateOf(0f) } - var snippetDuration by remember { mutableFloatStateOf(0f) } - var pointsToWin by remember { mutableFloatStateOf(0f) } - var playlistLink by remember { mutableStateOf("") } + 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( topBar = { TopAppBar( - title = { Text("New room") }, + title = { Text(stringResource(R.string.new_room)) }, navigationIcon = { - IconButton(onClick = { /* Handle back navigation */ }) { - Icon(Icons.Filled.KeyboardArrowLeft, contentDescription = "Back") + IconButton(onClick = { navController.popBackStack() }) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = stringResource(R.string.back) + ) } } ) @@ -66,55 +68,52 @@ fun SettingsScreen( verticalArrangement = Arrangement.spacedBy(24.dp) ) { SettingSlider( - label = "Number of players", + label = stringResource(R.string.number_of_players), value = maxPlayers, - onValueChange = { maxPlayers = it }, - valueRange = 1f..5f, + onValueChange = { viewModel.setMaxPlayers(it.toInt()) }, + valueRange = 1..5, steps = 4 ) SettingSlider( - label = "Snippet duration", + label = stringResource(R.string.snippet_duration), value = snippetDuration, - onValueChange = { snippetDuration = it }, - valueRange = 5f..30f, - steps = 4 + onValueChange = { viewModel.setSnippetDuration(it.toInt()) }, + valueRange = 5..30, + steps = 5 ) SettingSlider( - label = "Points to win", + label = stringResource(R.string.points_to_win), value = pointsToWin, - onValueChange = { pointsToWin = it }, - valueRange = 3f..10f, + onValueChange = { viewModel.setPointsToWin(it.toInt()) }, + valueRange = 3..10, steps = 6 ) Text( - text = "Playlist link", + text = stringResource(R.string.playlist_link), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp) ) TextField( value = playlistLink, - onValueChange = { newValue -> - playlistLink = newValue - viewModel.setPlaylistLink(newValue) - }, + onValueChange = { viewModel.setPlaylistLink(it) }, modifier = Modifier .fillMaxWidth() .height(56.dp) .padding(bottom = 16.dp), - label = { Text("Enter playlist link") } + label = { Text(stringResource(R.string.enter_playlist_link)) } ) Button( - onClick = { /*TODO home screen*/ }, + onClick = { navController.popBackStack() }, modifier = Modifier .fillMaxWidth() .height(56.dp) ) { - Text("Create room") + Text(stringResource(R.string.create_room)) } } } @@ -124,9 +123,9 @@ fun SettingsScreen( @Composable fun SettingSlider( label: String, - value: Float, - onValueChange: (Float) -> Unit, - valueRange: ClosedRange, + value: Int, + onValueChange: (Int) -> Unit, + valueRange: IntRange, steps: Int, modifier: Modifier = Modifier ) { @@ -141,25 +140,24 @@ fun SettingSlider( horizontalArrangement = Arrangement.SpaceBetween ) { Slider( - value = value, - onValueChange = onValueChange, - valueRange = valueRange as ClosedFloatingPointRange, - steps = steps, + 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.toInt().toString(), + text = value.toString(), style = MaterialTheme.typography.bodyMedium ) } } } - @Preview(showBackground = true, widthDp = 360, heightDp = 640) @Composable fun PreviewSettingsScreen() { val navController = rememberNavController() - SettingsScreen(parentNavController = navController) + NewRoomSettingsScreen(navController = navController) } \ No newline at end of file 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 index 82fc93f..6d41521 100644 --- 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 @@ -3,20 +3,21 @@ 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 + val maxPlayers: StateFlow = _maxPlayers.asStateFlow() private val _snippetDuration = MutableStateFlow(0) - val snippetDuration: StateFlow = _snippetDuration + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() private val _pointsToWin = MutableStateFlow(0) - val pointsToWin: StateFlow = _pointsToWin + val pointsToWin: StateFlow = _pointsToWin.asStateFlow() private val _playlistLink = MutableStateFlow("") - val playlistLink: StateFlow get() = _playlistLink + val playlistLink: StateFlow get() = _playlistLink.asStateFlow() fun setMaxPlayers(value: Int) { 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 index 3e02565..03e99ef 100644 --- 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 @@ -17,96 +17,74 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +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.NavHostController -import androidx.navigation.compose.currentBackStackEntryAsState +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( - parentNavController: NavHostController, viewModel: StartGameViewModel = StartGameViewModel(), - onCountdownFinish: () -> Unit + navController: NavController ) { - val navController = rememberNavController() - val currentBackStack by navController.currentBackStackEntryAsState() - val title = currentBackStack?.destination?.route ?: "FeelBeat" - - val players by viewModel.players.collectAsState() - var countdown by remember { mutableStateOf(3) } + val players by viewModel.players.collectAsState() + var countdown by remember { mutableIntStateOf(3) } - LaunchedEffect(key1 = countdown) { - if (countdown > 0) { - kotlinx.coroutines.delay(1000) - countdown -= 1 - } else { - onCountdownFinish() - } + LaunchedEffect(key1 = countdown) { + if (countdown > 0) { + kotlinx.coroutines.delay(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) - } + 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, - //color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(top = 32.dp) - ) - } + 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 = "Player Image", - modifier = Modifier - .size(80.dp) + 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) - ) - } + .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 nav = rememberNavController() - val onCountdownFinish = { println("Countdown finished!") } - StartGameScreen (nav, onCountdownFinish = onCountdownFinish) -} \ No newline at end of file + 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 index d79b999..9b4e43e 100644 --- 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 @@ -11,21 +11,21 @@ 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() + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players.asStateFlow() - init { - loadPlayers() - } + 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 - } + 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/values/strings.xml b/app/src/main/res/values/strings.xml index 179877d..a95191a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,26 @@ 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 Authorization server is unreachable \ No newline at end of file From 5879b1fefa378afe57a5faad66d221ad27d28245 Mon Sep 17 00:00:00 2001 From: vltkv <119106723+vltkv@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:49:37 +0100 Subject: [PATCH 4/7] Add whole navigation and GameResultScreen --- .../androidclient/ui/FeelBeatApp.kt | 7 +- .../androidclient/ui/FeelBeatRoute.kt | 1 + .../androidclient/ui/MainActivity.kt | 6 +- .../ui/acceptGame/AcceptGameScreen.kt | 144 +++++++------- .../ui/acceptGame/AcceptGameViewModel.kt | 91 ++++----- .../ui/gameResult/GameResultScreen.kt | 88 +++++++++ .../ui/gameResult/GameResultViewModel.kt | 34 ++++ .../ui/guessSong/GuessResultScreen.kt | 66 ++++--- .../ui/guessSong/GuessSongScreen.kt | 119 +++++++----- .../ui/guessSong/GuessSongViewModel.kt | 84 +++++---- .../ui/guessSong/ResultStatus.kt | 6 +- .../androidclient/ui/home/HomeScreen.kt | 3 - .../androidclient/ui/home/HomeViewModel.kt | 82 ++++---- .../androidclient/ui/login/LoginScreen.kt | 16 +- .../newRoomSettings/NewRoomSettingsScreen.kt | 176 ++++++++---------- .../NewRoomSettingsViewModel.kt | 43 +++-- .../ui/roomSettings/RoomSettingsScreen.kt | 150 +++++++++++++++ .../ui/roomSettings/RoomSettingsViewModel.kt | 46 +++++ .../ui/startGame/StartGameScreen.kt | 4 +- app/src/main/res/values/strings.xml | 6 + 20 files changed, 768 insertions(+), 404 deletions(-) create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsScreen.kt create mode 100644 app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsViewModel.kt 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 59179a4..3b471fd 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 @@ -9,11 +9,13 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController 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 @@ -34,6 +36,9 @@ fun FeelBeatApp( 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) } @@ -41,7 +46,7 @@ fun FeelBeatApp( // AccountSettingsScreen(parentNavController = navController) } composable(route = FeelBeatRoute.GAME_RESULT.name) { - // GameResultScreen(navController = navController) + GameResultScreen(navController = navController) } composable(route = FeelBeatRoute.GUESS_SONG.name) { GuessSongScreen(navController = navController) 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 4592e8e..5ee16fe 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 @@ -4,6 +4,7 @@ enum class FeelBeatRoute { LOGIN, HOME, NEW_ROOM_SETTINGS, + ROOM_SETTINGS, ACCEPT_GAME, ACCOUNT_SETTINGS, GAME_RESULT, 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..bb3dc1e 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,7 +7,6 @@ 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 @@ -21,11 +20,10 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() -// socket.connect("/ws") + // socket.connect("/ws") val startRoute = - if (authManager.isAuthenticated()) FeelBeatRoute.HOME - else FeelBeatRoute.LOGIN + if (authManager.isAuthenticated()) FeelBeatRoute.HOME else FeelBeatRoute.LOGIN setContent { val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass 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 index 1fe3801..a0692ea 100644 --- 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 @@ -1,8 +1,8 @@ package com.github.feelbeatapp.androidclient.ui.acceptGame -import androidx.compose.foundation.Image -import androidx.compose.foundation.border +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 @@ -10,23 +10,23 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape 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.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 @@ -34,86 +34,102 @@ 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.Player import com.github.feelbeatapp.androidclient.ui.startGame.PlayerCard @Composable -fun AcceptGameScreen(viewModel: AcceptViewModel = AcceptViewModel(), navController: NavController) { - val players = viewModel.players.collectAsState().value - val playlist = viewModel.playlist.collectAsState().value - val snippetDuration = viewModel.snippetDuration.collectAsState().value - val pointsToWin = viewModel.pointsToWin.collectAsState().value +fun AcceptGameScreen( + viewModel: AcceptGameViewModel = AcceptGameViewModel(), + navController: NavController, + // isRoomCreator: Boolean = false + isRoomCreator: Boolean = true +) { + val gameState = viewModel.gameState.collectAsState().value - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }) { - Icon( - Icons.AutoMirrored.Filled.KeyboardArrowLeft, - contentDescription = stringResource(R.string.back)) - } - - Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { - players.forEach { player -> PlayerCard(player = player) } - } + 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)) + } - Spacer(modifier = Modifier.height(32.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically) { + gameState.players.forEach { player -> PlayerCard(player = player) } + } - Column(horizontalAlignment = Alignment.Start) { - Text( - text = stringResource(id = R.string.snippet_duration_val, snippetDuration), - style = MaterialTheme.typography.bodyMedium) - Text( - text = stringResource(id = R.string.points_to_win_val, pointsToWin), - style = MaterialTheme.typography.bodyMedium) - } + Spacer(modifier = Modifier.height(32.dp)) - 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) + } - Text( - text = stringResource(id = R.string.playlist_name, playlist.name), - style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(32.dp)) - Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.playlist_name, gameState.playlist.name), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 16.dp)) - Column(modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxWidth()) { - playlist.songs.forEach { song -> SongItem(song = song) } - } + gameState.playlist.songs.forEach { song -> SongItem(song = song) } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(32.dp)) - Button( - onClick = { navController.navigate(FeelBeatRoute.START_GAME.name) }, - modifier = Modifier.fillMaxWidth(), - shape = CircleShape) { - Text(stringResource(R.string.play), style = MaterialTheme.typography.headlineMedium) + 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 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(20.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)) - } +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), + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = song.title, 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 index e8fa248..1da4206 100644 --- 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 @@ -1,7 +1,5 @@ package com.github.feelbeatapp.androidclient.ui.acceptGame -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.feelbeatapp.androidclient.R @@ -13,57 +11,48 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch data class Song(val id: Int, val title: String) -data class Player(val name: String, val image: Int) -data class Playlist(val name: String, val songs: List) - -//klasa mająca wszystkie stany -> jedno mutable state flow - -class AcceptViewModel : ViewModel() { - private val _songs = MutableStateFlow>(emptyList()) - val rooms: StateFlow> = _songs.asStateFlow() - - private val _selectedRoom= MutableStateFlow(null) - val selectedRoom: StateFlow = _selectedRoom.asStateFlow() - - private val _players = MutableStateFlow>(emptyList()) - val players: StateFlow> = _players.asStateFlow() - - private val _playlist = MutableStateFlow(Playlist(name = "Playlist #1", songs = emptyList())) - val playlist: StateFlow = _playlist.asStateFlow() - private val _snippetDuration = MutableStateFlow(30) - val snippetDuration: StateFlow = _snippetDuration.asStateFlow() - - private val _pointsToWin = MutableStateFlow(100) - val pointsToWin: StateFlow = _pointsToWin.asStateFlow() - - init { - loadPlayers() - loadSongs() - } +data class Playlist(val name: String, val songs: List) - 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 - //update zamiast value - } +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.emit(_gameState.value.copy(players = examplePlayers)) } - - 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"), - ) - _songs.value = exampleSongs - } + } + + 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.emit(_gameState.value.copy(songs = exampleSongs)) } + } } 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 index 7d14314..ae5b5a3 100644 --- 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 @@ -1,2 +1,90 @@ 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 index 7d14314..a639872 100644 --- 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 @@ -1,2 +1,36 @@ 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() + } + + 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 index ebb32a6..e07efd0 100644 --- 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 @@ -10,6 +10,7 @@ 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 @@ -19,20 +20,21 @@ 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 players by viewModel.players.collectAsState() - val currentSong by viewModel.currentSong.collectAsState() - val result by viewModel.result.collectAsState() + val guessState by viewModel.guessState.collectAsState() Column( modifier = Modifier.fillMaxSize().padding(16.dp), @@ -44,37 +46,42 @@ fun GuessResultScreen( Row( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { - players.forEach { player -> + guessState.players.forEach { playerWithResult -> PlayerStatusIcon( - image = player.image, isCorrect = (player.status == ResultStatus.CORRECT)) + image = playerWithResult.player.image, + isCorrect = (playerWithResult.resultStatus == ResultStatus.CORRECT)) } } - currentSong?.let { song -> + + guessState.currentSong?.let { song -> Box( modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth(), contentAlignment = Alignment.Center) { - SongInfo(songName = song.name, artistName = song.artist) + SongInfo(songTitle = song.title) } } } - result?.let { - Text( - text = if (it.isCorrect) "You guessed the song correctly!" else "You guessed wrong!", - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - color = - if (it.isCorrect) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.error, - modifier = Modifier.padding(top = 16.dp)) - } + 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)) - result?.let { - Text( - text = "Points: ${it.points}", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 32.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") } } } @@ -96,24 +103,15 @@ fun PlayerStatusIcon(image: Int, isCorrect: Boolean) { } @Composable -fun SongInfo(songName: String, artistName: String) { +fun SongInfo(songTitle: String) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - // Image( - // painter = painterResource(id = R.drawable.ic_playlist_image), // Replace with - // song image resource - // contentDescription = "Song Image", - // modifier = Modifier - // .size(50.dp) - // .clip(CircleShape) - // ) Column { Text( - text = songName, + text = songTitle, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold) - Text(text = "By $artistName", style = MaterialTheme.typography.bodySmall) } } } 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 index c1b136f..8cef293 100644 --- 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 @@ -15,6 +15,8 @@ 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 @@ -24,8 +26,12 @@ 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 @@ -39,6 +45,8 @@ 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 @@ -46,69 +54,89 @@ fun GuessSongScreen( navController: NavController, viewModel: GuessSongViewModel = GuessSongViewModel() ) { - val players by viewModel.players.collectAsState() - val playlist by viewModel.playlist.collectAsState() - val searchQuery by viewModel.searchQuery.collectAsState() + val guessState by viewModel.guessState.collectAsState() + var timeLeft by remember { mutableIntStateOf(guessState.snippetDuration) } - Scaffold(topBar = { TopAppBar(title = { Text("Playlist#1") }) }) { 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()) { - players.forEach { player -> PlayerStatusIcon(player = player) } - } + LaunchedEffect(key1 = timeLeft) { + if (timeLeft > 0) { + delay(timeMillis = 1000) + timeLeft -= 1 + } else { + navController.navigate(FeelBeatRoute.GUESS_RESULT.name) + } + } - MusicControlSlider() + 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) + } + } - Column { - Text( - text = stringResource(R.string.guess_the_song), - style = MaterialTheme.typography.bodyMedium) - SearchBar( - searchQuery = searchQuery, - onSearchQueryChange = { viewModel.updateSearchQuery(it) }) - } + MusicControlSlider() - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize()) { - items(playlist.size) { index -> - SongItem( - song = playlist[index], - onClick = { navController.navigate(FeelBeatRoute.GUESS_RESULT.name) }) - } + 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: Player1) { +fun PlayerStatusIcon(player: PlayerWithResult) { val icon = - when (player.status) { - // PlayerStatus.CORRECT -> Icons.Outlined.CheckCircle - // PlayerStatus.WRONG -> Icons.Outlined.Error + when (player.resultStatus) { + ResultStatus.CORRECT -> Icons.Outlined.Done + ResultStatus.WRONG -> Icons.Outlined.Close ResultStatus.NORESPONSE -> null - else -> null } val color = - when (player.status) { - // PlayerStatus.CORRECT -> Color.Green - // PlayerStatus.WRONG -> Color.Red - // PlayerStatus.UNANSWERED -> Color.Gray - else -> null + 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.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)) } @@ -150,12 +178,7 @@ fun SongItem(song: Song, onClick: () -> Unit) { modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - Column { Text(song.name, style = MaterialTheme.typography.bodyLarge) } - // Image( - // painter = painterResource(id = song.image), - // contentDescription = null, - // modifier = Modifier.size(48.dp) - // ) + Column { Text(song.title, style = MaterialTheme.typography.bodyLarge) } } } } 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 index c103fa9..e0a12e3 100644 --- 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 @@ -4,31 +4,35 @@ 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 -// dodatkowa klasa na wszystko, -data class Player1(val name: String, val image: Int, val status: ResultStatus) -data class Song(val id: Int, val name: String, val artist: String) -data class Result(val isCorrect: Boolean, val points: Int) +data class PlayerWithResult( + val player: Player, + val resultStatus: ResultStatus, + val points: Int +) -class GuessSongViewModel : ViewModel() { - private val _players = MutableStateFlow>(emptyList()) - val players: StateFlow> = _players - - private val _playlist = MutableStateFlow>(emptyList()) - val playlist: StateFlow> = _playlist - - private val _searchQuery = MutableStateFlow(TextFieldValue("")) - val searchQuery: StateFlow = _searchQuery - - private val _currentSong = MutableStateFlow(null) - val currentSong: StateFlow = _currentSong +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, +) - private val _result = MutableStateFlow(null) - val result: StateFlow = _result +class GuessSongViewModel : ViewModel() { + private val _guessState = MutableStateFlow(GuessState()) + val guessState: StateFlow = _guessState.asStateFlow() init { loadPlayers() @@ -38,36 +42,50 @@ class GuessSongViewModel : ViewModel() { private fun loadPlayers() { viewModelScope.launch { val examplePlayers = listOf( - Player1("User123", R.drawable.userimage, ResultStatus.CORRECT), - Player1("User456", R.drawable.userimage, ResultStatus.WRONG), - Player1("User789", R.drawable.userimage, ResultStatus.NORESPONSE) + 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) ) - _players.value = examplePlayers + _guessState.value = _guessState.value.copy(players = examplePlayers) } } private fun loadPlaylist() { viewModelScope.launch { val examplePlaylist = listOf( - Song(1, "Song 1", "artist1"), - Song(2, "Song 2", "artist1"), - Song(3, "Song 3", "artist1"), - Song(4, "Song 4", "artist1"), - Song(5, "Song 5", "artist1"), - Song(6, "Song 6", "artist1") + Song(1, "Song 1"), + Song(2, "Song 2"), + Song(3, "Song 3"), + Song(4, "Song 4"), + Song(5, "Song 5"), + Song(6, "Song 6") ) - _playlist.value = examplePlaylist + _guessState.value = _guessState.value.copy(songs = examplePlaylist, currentSong = examplePlaylist[0]) } } fun updateSearchQuery(newQuery: TextFieldValue) { - _searchQuery.value = newQuery + _guessState.value = _guessState.value.copy(searchQuery = newQuery) } - fun submitAnswer(isCorrect: Boolean) { + fun submitAnswer(playerName: String, isCorrect: Boolean) { viewModelScope.launch { - val points = if (isCorrect) 10 else 0 - _result.value = Result(isCorrect = isCorrect, points = points) + 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 index de8369d..2326181 100644 --- 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 @@ -1,5 +1,7 @@ package com.github.feelbeatapp.androidclient.ui.guessSong enum class ResultStatus { - CORRECT, WRONG, NORESPONSE -} \ No newline at end of file + CORRECT, + WRONG, + NORESPONSE +} 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 c99b14b..2aefd1b 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 @@ -84,9 +84,6 @@ fun HomeScreen( tint = MaterialTheme.colorScheme.onPrimary) } } - // TODO wywalic next -> po kliknieciu na playliste -> accept screen, w którym admin może - // jeszcze raz edytowac ustawieina, accept screen pasek na dole albo widok pokoju albo - // ustawienia? } } } 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 673b94c..a1c5728 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 @@ -7,41 +7,55 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -//TODO doac graczy, playliste, ustawienia -data class Room(val id: Int, val name: String) +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() - } - - private fun loadRooms() { - viewModelScope.launch { - val exampleRooms = listOf( - Room(1, "Pokoj 1"), - Room(2, "Pokoj 2"), - Room(3, "Pokoj 3"), - Room(4, "Pokoj 4"), - Room(5, "Pokoj 5"), - Room(6, "Pokoj 6"), - Room(7, "Pokoj 7") - ) - _rooms.value = exampleRooms - } - } - - fun selectRoom(room: Room) { - _selectedRoom.value = room - } - - fun addRoom(name: String) { - val newRoom = Room(id = _rooms.value.size + 1, name = name) - _rooms.value = _rooms.value + newRoom + private val _rooms = MutableStateFlow>(emptyList()) + val rooms: StateFlow> = _rooms.asStateFlow() + + private val _selectedRoom = MutableStateFlow(null) + val selectedRoom: StateFlow = _selectedRoom.asStateFlow() + + init { + loadRooms() + } + + 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 } -} \ No newline at end of file + } + + 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 = _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 e174d41..eaa35bf 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 @@ -13,12 +13,12 @@ import com.github.feelbeatapp.androidclient.R @Composable fun LoginScreen(onLoggedIn: () -> Unit, modifier: Modifier = Modifier) { - Column( - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.fillMaxSize(), - ) { - Text(stringResource(R.string.login_screen)) - Button(onClick = onLoggedIn) { Text(stringResource(R.string.login_with_spotify)) } - } + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxSize(), + ) { + Text(stringResource(R.string.login_screen)) + Button(onClick = onLoggedIn) { 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 index 6565382..f152d73 100644 --- 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 @@ -40,84 +40,72 @@ fun NewRoomSettingsScreen( navController: NavController, modifier: Modifier = Modifier ) { - val maxPlayers by viewModel.maxPlayers.collectAsState() - val snippetDuration by viewModel.snippetDuration.collectAsState() - val pointsToWin by viewModel.pointsToWin.collectAsState() - val playlistLink by viewModel.playlistLink.collectAsState() + 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) - ) { - SettingSlider( - label = stringResource(R.string.number_of_players), - value = maxPlayers, - onValueChange = { viewModel.setMaxPlayers(it.toInt()) }, - valueRange = 1..5, - steps = 4 - ) + 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) - SettingSlider( - label = stringResource(R.string.snippet_duration), - value = snippetDuration, - onValueChange = { viewModel.setSnippetDuration(it.toInt()) }, - valueRange = 5..30, - steps = 5 - ) + Text( + text = stringResource(R.string.playlist_link), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp)) - SettingSlider( - label = stringResource(R.string.points_to_win), - value = pointsToWin, - onValueChange = { viewModel.setPointsToWin(it.toInt()) }, - valueRange = 3..10, - steps = 6 - ) + 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)) }) - 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) - ) { + 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 @@ -129,35 +117,27 @@ fun SettingSlider( 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 - ) + 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) -} \ No newline at end of file + 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 index 6d41521..ea7bd3d 100644 --- 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 @@ -7,33 +7,32 @@ import kotlinx.coroutines.flow.asStateFlow class NewRoomSettingsViewModel : ViewModel() { - private val _maxPlayers = MutableStateFlow(0) - val maxPlayers: StateFlow = _maxPlayers.asStateFlow() + private val _maxPlayers = MutableStateFlow(0) + val maxPlayers: StateFlow = _maxPlayers.asStateFlow() - private val _snippetDuration = MutableStateFlow(0) - val snippetDuration: StateFlow = _snippetDuration.asStateFlow() + private val _snippetDuration = MutableStateFlow(0) + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() - private val _pointsToWin = MutableStateFlow(0) - val pointsToWin: StateFlow = _pointsToWin.asStateFlow() + private val _pointsToWin = MutableStateFlow(0) + val pointsToWin: StateFlow = _pointsToWin.asStateFlow() - private val _playlistLink = MutableStateFlow("") - val playlistLink: StateFlow get() = _playlistLink.asStateFlow() + private val _playlistLink = MutableStateFlow("") + val playlistLink: StateFlow + get() = _playlistLink.asStateFlow() + fun setMaxPlayers(value: Int) { + _maxPlayers.value = value + } - fun setMaxPlayers(value: Int) { - _maxPlayers.value = value - } + fun setSnippetDuration(value: Int) { + _snippetDuration.value = value + } - fun setSnippetDuration(value: Int) { - _snippetDuration.value = value - } - - fun setPointsToWin(value: Int) { - _pointsToWin.value = value - } - - fun setPlaylistLink(value: String) { - _playlistLink.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..2daed97 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomSettings/RoomSettingsScreen.kt @@ -0,0 +1,150 @@ +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.ExperimentalMaterial3Api +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 + +@OptIn(ExperimentalMaterial3Api::class) +@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..4bc13fb --- /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 index 03e99ef..94df3c3 100644 --- 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 @@ -38,11 +38,11 @@ fun StartGameScreen( navController: NavController ) { val players by viewModel.players.collectAsState() - var countdown by remember { mutableIntStateOf(3) } + var countdown by remember { mutableIntStateOf(value = 3) } LaunchedEffect(key1 = countdown) { if (countdown > 0) { - kotlinx.coroutines.delay(1000) + kotlinx.coroutines.delay(timeMillis = 1000) countdown -= 1 } else { navController.navigate(FeelBeatRoute.GUESS_SONG.name) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a95191a..f06b46e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,5 +25,11 @@ 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 From 07ae3c62f81198b18dd0de32133990e3155764d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Mon, 23 Dec 2024 19:53:18 +0100 Subject: [PATCH 5/7] Review changes and reformat --- .../androidclient/ui/FeelBeatApp.kt | 70 +++--- .../androidclient/ui/FeelBeatRoute.kt | 20 +- .../androidclient/ui/MainActivity.kt | 24 +-- .../ui/acceptGame/AcceptGameScreen.kt | 132 +++++++----- .../ui/acceptGame/AcceptGameViewModel.kt | 64 +++--- .../ui/gameResult/GameResultScreen.kt | 64 +++--- .../ui/gameResult/GameResultViewModel.kt | 45 ++-- .../ui/guessSong/GuessResultScreen.kt | 110 +++++----- .../ui/guessSong/GuessSongScreen.kt | 201 ++++++++++-------- .../ui/guessSong/GuessSongViewModel.kt | 71 ++++--- .../ui/guessSong/ResultStatus.kt | 6 +- .../androidclient/ui/home/HomeScreen.kt | 163 +++++++------- .../androidclient/ui/home/HomeViewModel.kt | 82 +++---- .../androidclient/ui/login/LoginScreen.kt | 16 +- .../newRoomSettings/NewRoomSettingsScreen.kt | 156 +++++++------- .../NewRoomSettingsViewModel.kt | 43 ++-- .../ui/roomSettings/RoomSettingsScreen.kt | 154 +++++++------- .../ui/roomSettings/RoomSettingsViewModel.kt | 54 ++--- .../ui/startGame/StartGameScreen.kt | 72 ++++--- .../ui/startGame/StartGameViewModel.kt | 29 +-- 20 files changed, 846 insertions(+), 730 deletions(-) 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 3b471fd..b328116 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 @@ -24,46 +24,48 @@ fun FeelBeatApp( @Suppress("UnusedParameter") widthSizeClass: WindowWidthSizeClass, modifier: Modifier = Modifier, ) { - FeelBeatTheme { - val navController = rememberNavController() + FeelBeatTheme { + val navController = rememberNavController() - Box(modifier = modifier) { - NavHost(navController, startDestination = FeelBeatRoute.LOGIN.name) { - composable(route = FeelBeatRoute.LOGIN.name) { - LoginScreen(onLoggedIn = { navController.navigate(FeelBeatRoute.HOME.name) }) + Box(modifier = modifier) { + NavHost(navController, startDestination = FeelBeatRoute.LOGIN.name) { + composable(route = FeelBeatRoute.LOGIN.name) { + LoginScreen(onLoggedIn = { navController.navigate(FeelBeatRoute.HOME.name) }) + } + composable(route = FeelBeatRoute.HOME.name) { + 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.HOME.name) { 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) - } - } } - } } @Preview @Composable fun AppPreview() { - FeelBeatApp(WindowWidthSizeClass.Compact) + FeelBeatApp(WindowWidthSizeClass.Compact) } 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 5ee16fe..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 @@ -1,14 +1,14 @@ package com.github.feelbeatapp.androidclient.ui enum class FeelBeatRoute { - LOGIN, - HOME, - NEW_ROOM_SETTINGS, - ROOM_SETTINGS, - ACCEPT_GAME, - ACCOUNT_SETTINGS, - GAME_RESULT, - GUESS_SONG, - GUESS_RESULT, - START_GAME + LOGIN, + HOME, + 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 bb3dc1e..c9fe79c 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 @@ -13,21 +13,13 @@ 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 - - setContent { - val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass - FeelBeatApp(widthSizeClass, startRoute) - } + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass + FeelBeatApp(widthSizeClass) } + } } 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 index a0692ea..aa67308 100644 --- 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 @@ -40,107 +40,125 @@ import com.github.feelbeatapp.androidclient.ui.startGame.PlayerCard fun AcceptGameScreen( viewModel: AcceptGameViewModel = AcceptGameViewModel(), navController: NavController, - // isRoomCreator: Boolean = false - isRoomCreator: Boolean = true + isRoomCreator: Boolean = true, ) { - val gameState = viewModel.gameState.collectAsState().value + 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)) - } + 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) { + 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)) + Spacer(modifier = Modifier.height(32.dp)) - Column( - horizontalAlignment = Alignment.Start, - modifier = Modifier.padding(horizontal = 16.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) + 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) - } + style = MaterialTheme.typography.bodyMedium, + ) + } - Spacer(modifier = Modifier.height(32.dp)) + 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)) + 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) } + gameState.playlist.songs.forEach { song -> SongItem(song = song) } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(32.dp)) - Button( - onClick = { navController.navigate(FeelBeatRoute.START_GAME.name) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.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)) + 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) { + NavigationBar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + ) { NavigationBarItem( icon = { - Icon(Icons.Filled.Home, contentDescription = stringResource(R.string.selected_room)) + 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) }) + onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + ) NavigationBarItem( icon = { - Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) + Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) }, label = { Text(stringResource(R.string.settings)) }, selected = false, - onClick = { navController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }) - } + 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) { + 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)) - } + modifier = Modifier.weight(1f), + ) + } } @Preview @Composable fun PreviewAcceptScreen() { - val navController = rememberNavController() - AcceptGameScreen(navController = navController) + 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 index 1da4206..205923a 100644 --- 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 @@ -8,6 +8,7 @@ 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) @@ -20,39 +21,42 @@ data class GameState( val selectedRoom: Room? = null, val playlist: Playlist = Playlist(name = "Playlist #1", songs = emptyList()), val snippetDuration: Int = 30, - val pointsToWin: Int = 10 + 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.emit(_gameState.value.copy(players = examplePlayers)) + 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) } + } } - } - - 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.emit(_gameState.value.copy(songs = exampleSongs)) + + 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/gameResult/GameResultScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultScreen.kt index ae5b5a3..7f9f12f 100644 --- 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 @@ -36,55 +36,61 @@ import com.github.feelbeatapp.androidclient.ui.guessSong.PlayerWithResult @Composable fun GameResultScreen( navController: NavController, - viewModel: GameResultViewModel = GameResultViewModel() + viewModel: GameResultViewModel = GameResultViewModel(), ) { - val players by viewModel.players.collectAsState() + val players by viewModel.players.collectAsState() - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { Text( text = "Game Results", style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold) + fontWeight = FontWeight.Bold, + ) LazyColumn( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(8.dp)) { - items(players) { player -> PlayerScoreItem(player = player) } - } + 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") - } - } + 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)) + 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) + 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 index a639872..d2b9c3d 100644 --- 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 @@ -12,25 +12,36 @@ import kotlinx.coroutines.launch class GameResultViewModel : ViewModel() { - private val _players = MutableStateFlow>(emptyList()) - val players: StateFlow> = _players + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players - init { - fetchGameResults() - } + init { + fetchGameResults() + } - 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 } + 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 + _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 index e07efd0..087ea07 100644 --- 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 @@ -32,93 +32,105 @@ import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute @Composable fun GuessResultScreen( navController: NavController, - viewModel: GuessSongViewModel = GuessSongViewModel() + viewModel: GuessSongViewModel = GuessSongViewModel(), ) { - val guessState by viewModel.guessState.collectAsState() + val guessState by viewModel.guessState.collectAsState() - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween) { + 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)) - } - } + 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 -> + guessState.currentSong?.let { song -> Box( modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth(), - contentAlignment = Alignment.Center) { - SongInfo(songTitle = song.title) - } - } + contentAlignment = Alignment.Center, + ) { + SongInfo(songTitle = song.title) + } } + } Text( text = if (guessState.players.any { it.resultStatus == ResultStatus.CORRECT }) { - stringResource(R.string.you_guessed_song_correctly) + stringResource(R.string.you_guessed_song_correctly) } else { - stringResource(R.string.ups_that_s_not_correct) + 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)) + 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)) + modifier = Modifier.padding(bottom = 32.dp), + ) Button(onClick = { navController.navigate(FeelBeatRoute.GUESS_SONG.name) }) { - Text(text = "NEXT") + 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)) - } + 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)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Column { - Text( - text = songTitle, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold) + Text( + text = songTitle, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + ) } - } + } } @Preview @Composable fun GuessResultScreenPreview() { - val nav = rememberNavController() - GuessResultScreen(navController = nav) + 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 index 8cef293..3c75008 100644 --- 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 @@ -52,140 +52,155 @@ import kotlinx.coroutines.delay @Composable fun GuessSongScreen( navController: NavController, - viewModel: GuessSongViewModel = GuessSongViewModel() + viewModel: GuessSongViewModel = GuessSongViewModel(), ) { - val guessState by viewModel.guessState.collectAsState() - var timeLeft by remember { mutableIntStateOf(guessState.snippetDuration) } + 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) + 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 -> + 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) - } - } + 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() + MusicControlSlider() - Column { + Column { Text( text = stringResource(R.string.guess_the_song), - style = MaterialTheme.typography.bodyMedium) + style = MaterialTheme.typography.bodyMedium, + ) SearchBar( searchQuery = guessState.searchQuery, - onSearchQueryChange = { viewModel.updateSearchQuery(it) }) - } + 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) }) - } - } + 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 - } + 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)) + 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()) - } + 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) { + 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)) + 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) } + 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) + 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 index e0a12e3..26b8d41 100644 --- 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 @@ -13,11 +13,7 @@ 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 PlayerWithResult(val player: Player, val resultStatus: ResultStatus, val points: Int) data class GuessState( val players: List = emptyList(), @@ -41,26 +37,41 @@ class GuessSongViewModel : ViewModel() { 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) - ) + 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) } } 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]) + 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]) } } @@ -70,17 +81,19 @@ class GuessSongViewModel : ViewModel() { 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 + 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) } } 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 index 2326181..aa06694 100644 --- 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 @@ -1,7 +1,7 @@ package com.github.feelbeatapp.androidclient.ui.guessSong enum class ResultStatus { - CORRECT, - WRONG, - NORESPONSE + CORRECT, + WRONG, + NORESPONSE, } 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 2aefd1b..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 @@ -45,99 +45,114 @@ fun HomeScreen( modifier: Modifier = Modifier, navController: NavController, ) { - val title = stringResource(R.string.feel_beat) - val rooms by viewModel.rooms.collectAsState() - val selectedRoom by viewModel.selectedRoom.collectAsState() + val title = stringResource(R.string.feel_beat) + val rooms by viewModel.rooms.collectAsState() + val selectedRoom by viewModel.selectedRoom.collectAsState() - Scaffold(topBar = { HomeTopBar(title, navController) }) { innerPadding -> - Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { - 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) }) + Scaffold(topBar = { HomeTopBar(title, navController) }) { innerPadding -> + Column(modifier = modifier.padding(innerPadding).fillMaxSize()) { + 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) }, + ) + } } - } - Box(modifier = Modifier.fillMaxWidth().padding(bottom = 80.dp, start = 16.dp, end = 16.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) - } + Box( + modifier = + Modifier.fillMaxWidth().padding(bottom = 80.dp, start = 16.dp, end = 16.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, + ) + } + } } - } + } } - } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeTopBar(title: String, navController: NavController) { - CenterAlignedTopAppBar( - title = { Text(title) }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary, - ), - actions = { - IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCOUNT_SETTINGS.name) }) { - Icon( - imageVector = Icons.Outlined.Person, - contentDescription = stringResource(R.string.account), - ) - } - }, - ) + CenterAlignedTopAppBar( + title = { Text(title) }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + actions = { + IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCOUNT_SETTINGS.name) }) { + Icon( + imageVector = Icons.Outlined.Person, + contentDescription = stringResource(R.string.account), + ) + } + }, + ) } @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)) { + 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)) + Text( + text = room.name, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + ) } - } + } } @Preview @Composable fun HomePreview() { - val navController = rememberNavController() - HomeScreen(navController = navController) + 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 a1c5728..15fe91f 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 @@ -13,49 +13,51 @@ data class Room( val maxPlayers: Int, val snippetDuration: Int, val pointsToWin: Int, - val playlistLink: String + 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() - } - - 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 + private val _rooms = MutableStateFlow>(emptyList()) + val rooms: StateFlow> = _rooms.asStateFlow() + + private val _selectedRoom = MutableStateFlow(null) + val selectedRoom: StateFlow = _selectedRoom.asStateFlow() + + init { + loadRooms() + } + + 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 } - } - - 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 = _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 eaa35bf..e174d41 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 @@ -13,12 +13,12 @@ import com.github.feelbeatapp.androidclient.R @Composable fun LoginScreen(onLoggedIn: () -> Unit, modifier: Modifier = Modifier) { - Column( - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.fillMaxSize(), - ) { - Text(stringResource(R.string.login_screen)) - Button(onClick = onLoggedIn) { Text(stringResource(R.string.login_with_spotify)) } - } + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxSize(), + ) { + Text(stringResource(R.string.login_screen)) + Button(onClick = onLoggedIn) { 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 index f152d73..58174e7 100644 --- 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 @@ -38,74 +38,84 @@ import com.github.feelbeatapp.androidclient.R fun NewRoomSettingsScreen( viewModel: NewRoomSettingsViewModel = NewRoomSettingsViewModel(), navController: NavController, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val playlistLink by viewModel.playlistLink.collectAsState() + 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) + 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)) + 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)) }) + 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)) { + 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() + 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.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.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) + SettingSlider( + label = stringResource(R.string.points_to_win), + value = pointsToWin, + onValueChange = { viewModel.setPointsToWin(it.toInt()) }, + valueRange = 3..10, + steps = 6, + ) } @Composable @@ -115,29 +125,31 @@ fun SettingSlider( onValueChange: (Int) -> Unit, valueRange: IntRange, steps: Int, - modifier: Modifier = Modifier + 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) + 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) + 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 index ea7bd3d..e8bde69 100644 --- 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 @@ -6,33 +6,32 @@ 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 _maxPlayers = MutableStateFlow(0) - val maxPlayers: StateFlow = _maxPlayers.asStateFlow() + private val _snippetDuration = MutableStateFlow(0) + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() - private val _snippetDuration = MutableStateFlow(0) - val snippetDuration: StateFlow = _snippetDuration.asStateFlow() + private val _pointsToWin = MutableStateFlow(0) + val pointsToWin: StateFlow = _pointsToWin.asStateFlow() - private val _pointsToWin = MutableStateFlow(0) - val pointsToWin: StateFlow = _pointsToWin.asStateFlow() + private val _playlistLink = MutableStateFlow("") + val playlistLink: StateFlow + get() = _playlistLink.asStateFlow() - private val _playlistLink = MutableStateFlow("") - val playlistLink: StateFlow - get() = _playlistLink.asStateFlow() + fun setMaxPlayers(value: Int) { + _maxPlayers.value = value + } - fun setMaxPlayers(value: Int) { - _maxPlayers.value = value - } + fun setSnippetDuration(value: Int) { + _snippetDuration.value = value + } - fun setSnippetDuration(value: Int) { - _snippetDuration.value = value - } + fun setPointsToWin(value: Int) { + _pointsToWin.value = value + } - fun setPointsToWin(value: Int) { - _pointsToWin.value = value - } - - fun setPlaylistLink(value: String) { - _playlistLink.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 index 2daed97..0d68aee 100644 --- 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 @@ -12,7 +12,6 @@ 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.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -34,62 +33,68 @@ import androidx.navigation.compose.rememberNavController import com.github.feelbeatapp.androidclient.R import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomSettingsScreen( viewModel: RoomSettingsViewModel = RoomSettingsViewModel(), navController: NavController, modifier: Modifier = Modifier, - isRoomCreator: Boolean = true + 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() + 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) + 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.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) + 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)) + 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)) }) + 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 @@ -99,52 +104,57 @@ fun SettingSlider( onValueChange: (Int) -> Unit, valueRange: IntRange, steps: Int, - modifier: Modifier = Modifier + 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) + 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) { + NavigationBar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + ) { NavigationBarItem( icon = { - Icon(Icons.Filled.Home, contentDescription = stringResource(R.string.selected_room)) + 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) }) + onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + ) NavigationBarItem( icon = { - Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) + Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings)) }, label = { Text(stringResource(R.string.settings)) }, selected = false, - onClick = { navController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }) - } + onClick = { navController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }, + ) + } } @Preview(showBackground = true, widthDp = 360, heightDp = 640) @Composable fun PreviewRoomSettingsScreen() { - val navController = rememberNavController() - RoomSettingsScreen(navController = navController) + 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 index 4bc13fb..0e2b276 100644 --- 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 @@ -8,39 +8,39 @@ import kotlinx.coroutines.flow.asStateFlow class RoomSettingsViewModel : ViewModel() { - private val _maxPlayers = MutableStateFlow(0) - val maxPlayers: StateFlow = _maxPlayers.asStateFlow() + private val _maxPlayers = MutableStateFlow(0) + val maxPlayers: StateFlow = _maxPlayers.asStateFlow() - private val _snippetDuration = MutableStateFlow(0) - val snippetDuration: StateFlow = _snippetDuration.asStateFlow() + private val _snippetDuration = MutableStateFlow(0) + val snippetDuration: StateFlow = _snippetDuration.asStateFlow() - private val _pointsToWin = MutableStateFlow(0) - val pointsToWin: StateFlow = _pointsToWin.asStateFlow() + private val _pointsToWin = MutableStateFlow(0) + val pointsToWin: StateFlow = _pointsToWin.asStateFlow() - private val _playlistLink = MutableStateFlow("") - val playlistLink: StateFlow - get() = _playlistLink.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 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 setMaxPlayers(value: Int) { + _maxPlayers.value = value + } - fun setSnippetDuration(value: Int) { - _snippetDuration.value = value - } + fun setSnippetDuration(value: Int) { + _snippetDuration.value = value + } - fun setPointsToWin(value: Int) { - _pointsToWin.value = value - } + fun setPointsToWin(value: Int) { + _pointsToWin.value = value + } - fun setPlaylistLink(value: String) { - _playlistLink.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 index 94df3c3..55972db 100644 --- 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 @@ -35,56 +35,60 @@ import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute @Composable fun StartGameScreen( viewModel: StartGameViewModel = StartGameViewModel(), - navController: NavController + navController: NavController, ) { - val players by viewModel.players.collectAsState() - var countdown by remember { mutableIntStateOf(value = 3) } + 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) + 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) { + 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) } + players.forEach { player -> PlayerCard(player = player) } } Text( text = countdown.toString(), style = MaterialTheme.typography.headlineLarge, - modifier = Modifier.padding(top = 32.dp)) - } + 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)) - } + 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) + 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 index 9b4e43e..3b2fcd1 100644 --- 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 @@ -11,21 +11,22 @@ 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() + private val _players = MutableStateFlow>(emptyList()) + val players: StateFlow> = _players.asStateFlow() - init { - loadPlayers() - } + 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 + 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 + } } - } } From 0d8f35087ecdc7506c139695e5e671a9edee6a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Mon, 23 Dec 2024 20:34:35 +0100 Subject: [PATCH 6/7] Update packages, sync versions, fix cancelling login --- .../androidclient/auth/AuthManager.kt | 2 ++ .../auth/spotify/SpotifyAuthManager.kt | 4 ++++ .../androidclient/ui/AuthActivity.kt | 1 + .../androidclient/ui/FeelBeatApp.kt | 7 +++--- .../androidclient/ui/MainActivity.kt | 23 +++++++++++-------- .../androidclient/ui/login/LoginScreen.kt | 10 ++++++-- gradle/libs.versions.toml | 4 ++-- 7 files changed, 35 insertions(+), 16 deletions(-) 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/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 b328116..cf75766 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 @@ -22,15 +22,16 @@ import com.github.feelbeatapp.androidclient.ui.theme.FeelBeatTheme @Composable fun FeelBeatApp( @Suppress("UnusedParameter") widthSizeClass: WindowWidthSizeClass, + startScreen: FeelBeatRoute, modifier: Modifier = Modifier, ) { FeelBeatTheme { val navController = rememberNavController() Box(modifier = modifier) { - NavHost(navController, startDestination = FeelBeatRoute.LOGIN.name) { + NavHost(navController, startDestination = startScreen.name) { composable(route = FeelBeatRoute.LOGIN.name) { - LoginScreen(onLoggedIn = { navController.navigate(FeelBeatRoute.HOME.name) }) + LoginScreen() } composable(route = FeelBeatRoute.HOME.name) { HomeScreen(navController = navController) @@ -67,5 +68,5 @@ fun FeelBeatApp( @Preview @Composable fun AppPreview() { - FeelBeatApp(WindowWidthSizeClass.Compact) + FeelBeatApp(WindowWidthSizeClass.Compact, FeelBeatRoute.LOGIN) } 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 c9fe79c..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,19 +7,24 @@ 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.network.fullduplex.NetworkAgent import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass - FeelBeatApp(widthSizeClass) + @Inject lateinit var authManager: AuthManager + + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val startRoute = + 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/login/LoginScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/login/LoginScreen.kt index e174d41..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 @@ -8,17 +8,23 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel import com.github.feelbeatapp.androidclient.R @Composable -fun LoginScreen(onLoggedIn: () -> Unit, modifier: Modifier = Modifier) { +fun LoginScreen(loginViewModel: LoginViewModel = hiltViewModel(), modifier: Modifier = Modifier) { + val ctx = LocalContext.current + Column( verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.fillMaxSize(), ) { Text(stringResource(R.string.login_screen)) - Button(onClick = onLoggedIn) { Text(stringResource(R.string.login_with_spotify)) } + Button(onClick = { loginViewModel.login(ctx) }) { + Text(stringResource(R.string.login_with_spotify)) + } } } 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" From 1a75e2f1e490c940e1306efcb862e64fe3cb604f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Mon, 23 Dec 2024 20:57:29 +0100 Subject: [PATCH 7/7] Fix linter, and unit tests --- .../auth/storage/PreferencesAuthStorage.kt | 2 -- .../feelbeatapp/androidclient/ui/FeelBeatApp.kt | 4 +--- .../ui/acceptGame/AcceptGameViewModel.kt | 1 + .../feelbeatapp/androidclient/ui/game/GameScreen.kt | 1 - .../androidclient/ui/game/GameViewModel.kt | 4 +--- .../ui/gameResult/GameResultViewModel.kt | 1 + .../ui/guessSong/GuessSongViewModel.kt | 1 + .../androidclient/ui/home/HomeViewModel.kt | 1 + .../res/drawable/{userimage.png => userimage.webp} | Bin 9 files changed, 6 insertions(+), 9 deletions(-) rename app/src/main/res/drawable/{userimage.png => userimage.webp} (100%) 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/FeelBeatApp.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/FeelBeatApp.kt index cf75766..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 @@ -30,9 +30,7 @@ fun FeelBeatApp( Box(modifier = modifier) { NavHost(navController, startDestination = startScreen.name) { - composable(route = FeelBeatRoute.LOGIN.name) { - LoginScreen() - } + composable(route = FeelBeatRoute.LOGIN.name) { LoginScreen() } composable(route = FeelBeatRoute.HOME.name) { HomeScreen(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 index 205923a..fd8fed5 100644 --- 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 @@ -46,6 +46,7 @@ class AcceptGameViewModel : ViewModel() { } } + @SuppressWarnings("MagicNumber") private fun loadSongs() { viewModelScope.launch { val 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/GameResultViewModel.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/gameResult/GameResultViewModel.kt index d2b9c3d..911df3c 100644 --- 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 @@ -19,6 +19,7 @@ class GameResultViewModel : ViewModel() { fetchGameResults() } + @SuppressWarnings("MagicNumber") private fun fetchGameResults() { viewModelScope.launch { val results = 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 index 26b8d41..e7a49ce 100644 --- 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 @@ -59,6 +59,7 @@ class GuessSongViewModel : ViewModel() { } } + @SuppressWarnings("MagicNumber") private fun loadPlaylist() { viewModelScope.launch { val examplePlaylist = 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 15fe91f..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 @@ -28,6 +28,7 @@ class HomeViewModel : ViewModel() { loadRooms() } + @SuppressWarnings("MagicNumber") private fun loadRooms() { viewModelScope.launch { val exampleRooms = diff --git a/app/src/main/res/drawable/userimage.png b/app/src/main/res/drawable/userimage.webp similarity index 100% rename from app/src/main/res/drawable/userimage.png rename to app/src/main/res/drawable/userimage.webp