diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cbc7fbf..d97330f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,6 +108,9 @@ dependencies { implementation(libs.coil.compose) implementation(libs.coil.network) + // Coil + implementation(libs.coil.kt.coil.compose) + // ktor implementation(libs.io.ktor.client) implementation(libs.io.ktor.content.negotiation) 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 5ab89b0..567f534 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 @@ -17,5 +17,5 @@ interface AuthManager { suspend fun getAccessToken(): String - fun logout(ctx: Context) + fun logout() } 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 370de2d..682f22e 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 @@ -130,7 +130,7 @@ constructor( } private fun hasExpired(expires: Instant): Boolean { - return expires >= Instant.now() + return expires <= Instant.now() } private suspend fun refreshAccessToken() { @@ -177,7 +177,7 @@ constructor( ) } - override fun logout(ctx: Context) { + override fun logout() { try { authStorage.clearAuthData() authData = null 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 02eb570..fdbc65f 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 @@ -37,12 +37,6 @@ fun FeelBeatApp( composable(route = FeelBeatRoute.NEW_ROOM_SETTINGS.name) { NewRoomSettingsScreen(navController = navController) } - composable(route = FeelBeatRoute.ROOM_SETTINGS.name) { - EditRoomSettingsScreen(navController = navController) - } - composable(route = FeelBeatRoute.ACCEPT_GAME.name) { - AcceptGameScreen(navController = navController) - } composable(route = FeelBeatRoute.GAME_RESULT.name) { GameResultScreen(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 c62cac5..8e05b4e 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 @@ -6,7 +6,6 @@ enum class FeelBeatRoute { NEW_ROOM_SETTINGS, ROOM_SETTINGS, ACCEPT_GAME, - ACCOUNT_SETTINGS, GAME_RESULT, GUESS_SONG, GUESS_RESULT, 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 d6d75cd..936ed73 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 @@ -10,6 +10,7 @@ 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.verticalScroll import androidx.compose.material.icons.Icons @@ -33,14 +34,16 @@ 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.model.Song +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute +import com.github.feelbeatapp.androidclient.ui.home.HomeRoute import com.github.feelbeatapp.androidclient.ui.startgame.PlayerCard @Composable fun AcceptGameScreen( viewModel: AcceptGameViewModel = AcceptGameViewModel(), navController: NavController, + internalNavController: NavController, isRoomCreator: Boolean = true, ) { val gameState = viewModel.gameState.collectAsState().value @@ -53,10 +56,12 @@ fun AcceptGameScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { - IconButton(onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }) { + Spacer(modifier = Modifier.height(24.dp)) + IconButton(onClick = { internalNavController.navigate(HomeRoute.HOME.name) }) { Icon( Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = stringResource(R.string.back), + modifier = Modifier.size(40.dp), ) } @@ -110,7 +115,7 @@ fun AcceptGameScreen( if (isRoomCreator) { BottomNavigationBar( - navController = navController, + internalNavController = internalNavController, modifier = Modifier.align(Alignment.BottomCenter), ) } @@ -118,7 +123,7 @@ fun AcceptGameScreen( } @Composable -fun BottomNavigationBar(navController: NavController, modifier: Modifier = Modifier) { +fun BottomNavigationBar(internalNavController: NavController, modifier: Modifier = Modifier) { NavigationBar( modifier = modifier, containerColor = MaterialTheme.colorScheme.surface, @@ -130,7 +135,7 @@ fun BottomNavigationBar(navController: NavController, modifier: Modifier = Modif }, label = { Text(stringResource(R.string.selected_room)) }, selected = false, - onClick = { navController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + onClick = { internalNavController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, ) NavigationBarItem( icon = { @@ -138,7 +143,7 @@ fun BottomNavigationBar(navController: NavController, modifier: Modifier = Modif }, label = { Text(stringResource(R.string.settings)) }, selected = false, - onClick = { navController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }, + onClick = { internalNavController.navigate(FeelBeatRoute.ROOM_SETTINGS.name) }, ) } } @@ -161,5 +166,5 @@ fun SongItem(song: Song) { @Composable fun PreviewAcceptScreen() { val navController = rememberNavController() - AcceptGameScreen(navController = navController) + AcceptGameScreen(navController = navController, internalNavController = navController) } 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 new file mode 100644 index 0000000..3e2bd53 --- /dev/null +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/home/HomeRoute.kt @@ -0,0 +1,7 @@ +package com.github.feelbeatapp.androidclient.ui.home + +enum class HomeRoute { + HOME, + ROOM_SETTINGS, + ACCEPT_GAME, +} 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 f6ed309..bba6293 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 @@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar @@ -35,17 +34,24 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import coil3.compose.AsyncImage import com.github.feelbeatapp.androidclient.R -import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute import com.github.feelbeatapp.androidclient.model.Room +import com.github.feelbeatapp.androidclient.ui.FeelBeatRoute +import com.github.feelbeatapp.androidclient.ui.acceptgame.AcceptGameScreen +import com.github.feelbeatapp.androidclient.ui.roomsettings.screens.EditRoomSettingsScreen @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,9 +61,9 @@ fun HomeScreen( navController: NavController, ) { val title = stringResource(R.string.feel_beat) - val rooms by viewModel.rooms.collectAsState() - val selectedRoom by viewModel.selectedRoom.collectAsState() - val ctx = LocalContext.current + val playerName by viewModel.playerName.collectAsState() + val playerImageUrl by viewModel.playerImageUrl.collectAsState() + val internalNavController = rememberNavController() var isBottomSheetVisible by remember { mutableStateOf(false) } @@ -65,58 +71,38 @@ fun HomeScreen( ModalBottomSheet(onDismissRequest = { isBottomSheetVisible = false }) { UserAccountBottomSheetContent( onLogoutClick = { - viewModel.logout(ctx) + viewModel.logout() isBottomSheetVisible = false navController.navigate(FeelBeatRoute.LOGIN.name) - } + }, + name = playerName.toString(), + imageUrl = playerImageUrl.toString(), ) } } - Scaffold(topBar = { HomeTopBar(title) { isBottomSheetVisible = true } }) { innerPadding -> + Scaffold( + topBar = { + HomeTopBar(title, imageUrl = playerImageUrl.toString()) { isBottomSheetVisible = true } + } + ) { 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) }, + NavHost(internalNavController, startDestination = HomeRoute.HOME.name) { + composable(route = HomeRoute.HOME.name) { + HomeBody( + viewModel = viewModel, + internalNavController = internalNavController, + navController = navController, ) } - } - - Box( - modifier = - Modifier.fillMaxWidth().padding(bottom = 80.dp, start = 16.dp, end = 16.dp) - ) { - Box( - modifier = - Modifier.align(Alignment.BottomEnd) - .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, - ) - } + composable(route = HomeRoute.ACCEPT_GAME.name) { + AcceptGameScreen( + internalNavController = internalNavController, + navController = navController, + ) + } + composable(route = HomeRoute.ROOM_SETTINGS.name) { + EditRoomSettingsScreen(internalNavController = internalNavController) } } } @@ -125,7 +111,7 @@ fun HomeScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeTopBar(title: String, onAccountClick: () -> Unit) { +fun HomeTopBar(title: String, imageUrl: String, onAccountClick: () -> Unit) { CenterAlignedTopAppBar( title = { Text(title) }, colors = @@ -135,9 +121,13 @@ fun HomeTopBar(title: String, onAccountClick: () -> Unit) { ), actions = { IconButton(onClick = onAccountClick) { - Icon( - imageVector = Icons.Outlined.Person, - contentDescription = stringResource(R.string.account), + AsyncImage( + model = imageUrl, + contentDescription = "Player Image", + modifier = Modifier.size(80.dp).clip(MaterialTheme.shapes.large), + contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.account), + error = painterResource(R.drawable.account), ) } }, @@ -145,7 +135,64 @@ fun HomeTopBar(title: String, onAccountClick: () -> Unit) { } @Composable -fun UserAccountBottomSheetContent(onLogoutClick: () -> Unit) { +fun HomeBody( + viewModel: HomeViewModel, + navController: NavController, + internalNavController: NavController, +) { + val rooms by viewModel.rooms.collectAsState() + val selectedRoom by viewModel.selectedRoom.collectAsState() + + Column(modifier = Modifier.fillMaxSize()) { + Text( + text = stringResource(R.string.current_games), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(16.dp), + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(rooms) { room -> + RoomItem( + room = room, + isSelected = room == selectedRoom, + onClick = { internalNavController.navigate(FeelBeatRoute.ACCEPT_GAME.name) }, + ) + } + } + + Box( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp).padding(horizontal = 16.dp) + ) { + Box( + modifier = + Modifier.align(Alignment.BottomEnd) + .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, + ) + } + } + } + } +} + +@Composable +fun UserAccountBottomSheetContent(onLogoutClick: () -> Unit, name: String, imageUrl: String) { Column( modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -156,13 +203,16 @@ fun UserAccountBottomSheetContent(onLogoutClick: () -> Unit) { Modifier.size(80.dp).background(Color.Gray, shape = MaterialTheme.shapes.large), contentAlignment = Alignment.Center, ) { - Text( - text = "U", // Placeholder for user's avatar - style = MaterialTheme.typography.headlineMedium, - color = Color.White, + AsyncImage( + model = imageUrl, + contentDescription = "Player Image", + modifier = Modifier.size(80.dp).clip(MaterialTheme.shapes.large), + contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.account), + error = painterResource(R.drawable.account), ) } - Text(text = "User Name", style = MaterialTheme.typography.titleMedium) + Text(text = name, style = MaterialTheme.typography.titleMedium) Box( modifier = Modifier.fillMaxWidth() 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 9ecf055..e884e2c 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,10 +1,10 @@ package com.github.feelbeatapp.androidclient.ui.home -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.feelbeatapp.androidclient.auth.AuthManager import com.github.feelbeatapp.androidclient.model.Room +import com.github.feelbeatapp.androidclient.network.spotify.KtorSpotifyAPI import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -13,19 +13,42 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @HiltViewModel -class HomeViewModel @Inject constructor(private val authManager: AuthManager) : ViewModel() { +class HomeViewModel +@Inject +constructor(private val authManager: AuthManager, private val spotifyAPI: KtorSpotifyAPI) : + ViewModel() { private val _rooms = MutableStateFlow>(emptyList()) val rooms: StateFlow> = _rooms.asStateFlow() private val _selectedRoom = MutableStateFlow(null) val selectedRoom: StateFlow = _selectedRoom.asStateFlow() + private val _playerName = MutableStateFlow(null) + val playerName: StateFlow = _playerName.asStateFlow() + + private val _playerImageUrl = MutableStateFlow(null) + val playerImageUrl: StateFlow = _playerImageUrl.asStateFlow() + init { loadRooms() + loadPlayerData() + } + + private fun loadPlayerData() { + viewModelScope.launch { + try { + val profile = spotifyAPI.getProfile() + _playerName.value = profile.displayName + _playerImageUrl.value = profile.images.first().url + } catch (e: Exception) { + print(e.message) + _playerName.value = "Player" + } + } } - fun logout(ctx: Context) { - authManager.logout(ctx) + fun logout() { + authManager.logout() } @SuppressWarnings("MagicNumber") diff --git a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomsettings/screens/EditRoomSettingsScreen.kt b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomsettings/screens/EditRoomSettingsScreen.kt index 67cb905..31373ca 100644 --- a/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomsettings/screens/EditRoomSettingsScreen.kt +++ b/app/src/main/java/com/github/feelbeatapp/androidclient/ui/roomsettings/screens/EditRoomSettingsScreen.kt @@ -34,7 +34,7 @@ import com.github.feelbeatapp.androidclient.ui.roomsettings.viewmodels.RoomSetti @Composable fun EditRoomSettingsScreen( viewModel: RoomSettingsViewModel = EditRoomSettingsViewModel(), - navController: NavController, + internalNavController: NavController, modifier: Modifier = Modifier, isRoomCreator: Boolean = true, ) { @@ -43,7 +43,7 @@ fun EditRoomSettingsScreen( Scaffold( bottomBar = { if (isRoomCreator) { - BottomNavigationBar(navController = navController) + BottomNavigationBar(navController = internalNavController) } }, content = { padding -> @@ -100,5 +100,5 @@ fun BottomNavigationBar(navController: NavController, modifier: Modifier = Modif @Composable fun PreviewRoomSettingsScreen() { val navController = rememberNavController() - EditRoomSettingsScreen(navController = navController) + EditRoomSettingsScreen(internalNavController = navController) } diff --git a/app/src/main/res/drawable/account.png b/app/src/main/res/drawable/account.png new file mode 100644 index 0000000..e6298a5 Binary files /dev/null and b/app/src/main/res/drawable/account.png differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3697e18..32b1256 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.7.3" +coilCompose = "2.4.0" fuzzywuzzy = "1.4.0" kotlin = "2.0.21" coreKtx = "1.15.0" @@ -24,6 +25,7 @@ coil = "3.0.4" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +coil-kt-coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } fuzzywuzzy = { module = "me.xdrop:fuzzywuzzy", version.ref = "fuzzywuzzy" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }