diff --git a/app/src/main/java/app/gamenative/ui/PluviaMain.kt b/app/src/main/java/app/gamenative/ui/PluviaMain.kt index 5d4d64a81..6d0ced206 100644 --- a/app/src/main/java/app/gamenative/ui/PluviaMain.kt +++ b/app/src/main/java/app/gamenative/ui/PluviaMain.kt @@ -58,6 +58,7 @@ import app.gamenative.ui.enums.Orientation import app.gamenative.ui.model.MainViewModel import app.gamenative.ui.screen.HomeScreen import app.gamenative.ui.screen.PluviaScreen +import app.gamenative.ui.screen.accounts.AccountManagementScreen import app.gamenative.ui.screen.chat.ChatScreen import app.gamenative.ui.screen.login.UserLoginScreen import app.gamenative.ui.screen.settings.SettingsScreen @@ -79,6 +80,8 @@ import timber.log.Timber import java.util.Date import java.util.EnumSet import kotlin.reflect.KFunction2 +import app.gamenative.ui.screen.accounts.AccountManagementScreen +import app.gamenative.ui.screen.welcome.WelcomeScreen @Composable fun PluviaMain( @@ -192,20 +195,16 @@ fun PluviaMain( } MainViewModel.MainUiEvent.OnLoggedOut -> { - // Pop stack and go back to login. - navController.popBackStack( - route = PluviaScreen.LoginUser.route, - inclusive = false, - saveState = false, - ) + // Do nothing - let users stay on current page after logout } is MainViewModel.MainUiEvent.OnLogonEnded -> { when (event.result) { LoginResult.Success -> { if (PluviaApp.xEnvironment == null) { - Timber.i("Navigating to library") - navController.navigate(PluviaScreen.Home.route) + navController.navigate(PluviaScreen.AccountManagement.route) { + popUpTo(PluviaScreen.LoginUser.route) { inclusive = true } + } // If a crash happen, lets not ask for a tip yet. // Instead, ask the user to contribute their issues to be addressed. @@ -312,7 +311,9 @@ fun PluviaMain( isConnecting = true context.startForegroundService(Intent(context, SteamService::class.java)) } - if (SteamService.isLoggedIn && state.currentScreen == PluviaScreen.LoginUser) { + // Only auto-navigate to home if user is logged in, on login screen, AND it's not first launch + if (SteamService.isLoggedIn && state.currentScreen == PluviaScreen.LoginUser && !state.isFirstLaunch) { + Timber.i("DEBUG: Auto-navigation triggered - SteamService.isLoggedIn=${SteamService.isLoggedIn}, currentScreen=${state.currentScreen}, isFirstLaunch=${state.isFirstLaunch}") navController.navigate(PluviaScreen.Home.route) } } @@ -694,9 +695,20 @@ fun PluviaMain( NavHost( navController = navController, - startDestination = PluviaScreen.LoginUser.route, + startDestination = PluviaScreen.Welcome.route, ) { - /** Login **/ + /** Welcome **/ + composable(route = PluviaScreen.Welcome.route) { + WelcomeScreen( + onSetupAccounts = { + navController.navigate(PluviaScreen.AccountManagement.route) + }, + onSkipSignIn = { + navController.navigate(PluviaScreen.Home.route + "?offline=true") + }, + ) + } + /** Login **/ composable(route = PluviaScreen.LoginUser.route) { UserLoginScreen( @@ -705,6 +717,18 @@ fun PluviaMain( }, ) } + + /** Account Management **/ + composable(route = PluviaScreen.AccountManagement.route) { + AccountManagementScreen( + onNavigateRoute = { + navController.navigate(it) + }, + onBack = { + navController.navigateUp() + }, + ) + } /** Library, Downloads, Friends **/ /** Library, Downloads, Friends **/ composable( @@ -743,9 +767,6 @@ fun PluviaMain( onNavigateRoute = { navController.navigate(it) }, - onLogout = { - SteamService.logOut() - }, onGoOnline = { navController.navigate(PluviaScreen.LoginUser.route) }, diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ProfileDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ProfileDialog.kt index 26665d65f..21a443588 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ProfileDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ProfileDialog.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Login import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog @@ -49,7 +50,6 @@ fun ProfileDialog( state: EPersonaState, onStatusChange: (EPersonaState) -> Unit, onNavigateRoute: (String) -> Unit, - onLogout: () -> Unit, onDismiss: () -> Unit, onGoOnline: () -> Unit, isOffline: Boolean = false, @@ -109,6 +109,13 @@ fun ProfileDialog( /* Action Buttons */ Spacer(modifier = Modifier.height(16.dp)) + + FilledTonalButton(modifier = Modifier.fillMaxWidth(), onClick = { onNavigateRoute(PluviaScreen.AccountManagement.route) }) { + Icon(imageVector = Icons.Default.AccountCircle, contentDescription = null) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSize)) + Text(text = "Manage Accounts") + } + FilledTonalButton(modifier = Modifier.fillMaxWidth(), onClick = { onNavigateRoute(PluviaScreen.Settings.route) }) { Icon(imageVector = Icons.Default.Settings, contentDescription = null) Spacer(modifier = Modifier.size(ButtonDefaults.IconSize)) @@ -127,12 +134,6 @@ fun ProfileDialog( Spacer(modifier = Modifier.size(ButtonDefaults.IconSize)) Text(text = "Go Online") } - } else { - FilledTonalButton(modifier = Modifier.fillMaxWidth(), onClick = onLogout) { - Icon(imageVector = Icons.AutoMirrored.Filled.Logout, contentDescription = null) - Spacer(modifier = Modifier.size(ButtonDefaults.IconSize)) - Text(text = "Log Out") - } } } }, @@ -155,7 +156,6 @@ private fun Preview_ProfileDialog() { state = EPersonaState.Online, onStatusChange = {}, onNavigateRoute = {}, - onLogout = {}, onDismiss = {}, onGoOnline = {}, ) diff --git a/app/src/main/java/app/gamenative/ui/component/topbar/AccountButton.kt b/app/src/main/java/app/gamenative/ui/component/topbar/AccountButton.kt index f97992756..5042b0144 100644 --- a/app/src/main/java/app/gamenative/ui/component/topbar/AccountButton.kt +++ b/app/src/main/java/app/gamenative/ui/component/topbar/AccountButton.kt @@ -29,7 +29,6 @@ import timber.log.Timber @Composable fun AccountButton( onNavigateRoute: (String) -> Unit, - onLogout: () -> Unit, onGoOnline: () -> Unit, isOffline: Boolean = false, ) { @@ -70,10 +69,6 @@ fun AccountButton( onNavigateRoute(it) showDialog = false }, - onLogout = { - onLogout() - showDialog = false - }, onGoOnline = { onGoOnline() showDialog = false @@ -105,7 +100,6 @@ private fun Preview_AccountButton() { actions = { AccountButton( onNavigateRoute = {}, - onLogout = {}, onGoOnline = {}, ) }, diff --git a/app/src/main/java/app/gamenative/ui/data/MainState.kt b/app/src/main/java/app/gamenative/ui/data/MainState.kt index fc9ccf133..3864dd859 100644 --- a/app/src/main/java/app/gamenative/ui/data/MainState.kt +++ b/app/src/main/java/app/gamenative/ui/data/MainState.kt @@ -18,4 +18,5 @@ data class MainState( val launchedAppId: String = "", val bootToContainer: Boolean = false, val showBootingSplash: Boolean = false, + val isFirstLaunch: Boolean = true, ) diff --git a/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt b/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt index 0ce3ec2dd..01db3c0cc 100644 --- a/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt +++ b/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt @@ -18,6 +18,8 @@ import app.gamenative.ui.enums.AppFilter import dagger.hilt.android.lifecycle.HiltViewModel import java.util.EnumSet import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,8 +27,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import kotlin.math.max -import kotlin.math.min @HiltViewModel class LibraryViewModel @Inject constructor( @@ -40,8 +40,8 @@ class LibraryViewModel @Inject constructor( var listState: LazyListState by mutableStateOf(LazyListState(0, 0)) // How many items loaded on one page of results - private var paginationCurrentPage: Int = 0; - private var lastPageInCurrentFilter: Int = 0; + private var paginationCurrentPage: Int = 0 + private var lastPageInCurrentFilter: Int = 0 // Complete and unfiltered app list private var appList: List = emptyList() @@ -156,8 +156,8 @@ class LibraryViewModel @Inject constructor( } .sortedWith( // Comes from DAO in alphabetical order - compareByDescending { downloadDirectoryApps.contains(SteamService.getAppDirName(it)) } - ); + compareByDescending { downloadDirectoryApps.contains(SteamService.getAppDirName(it)) }, + ) // Total count for the current filter val totalFound = filteredList.count() @@ -183,14 +183,14 @@ class LibraryViewModel @Inject constructor( } .toList() - Timber.tag("LibraryViewModel").d("Filtered list size: ${totalFound}") + Timber.tag("LibraryViewModel").d("Filtered list size: $totalFound") _state.update { it.copy( appInfoList = filteredListPage, currentPaginationPage = paginationPage + 1, // visual display is not 0 indexed lastPaginationPage = lastPageInCurrentFilter + 1, totalAppsInFilter = totalFound, - ) + ) } } } diff --git a/app/src/main/java/app/gamenative/ui/model/MainViewModel.kt b/app/src/main/java/app/gamenative/ui/model/MainViewModel.kt index b34048bb0..642eaa40c 100644 --- a/app/src/main/java/app/gamenative/ui/model/MainViewModel.kt +++ b/app/src/main/java/app/gamenative/ui/model/MainViewModel.kt @@ -131,6 +131,15 @@ class MainViewModel @Inject constructor( _state.update { it.copy(paletteStyle = value) } } } + + _state.update { + it.copy( + isFirstLaunch = true, // Temporarily always true for debugging + isSteamConnected = SteamService.isConnected, + hasCrashedLastStart = PrefManager.recentlyCrashed, + launchedAppId = "", + ) + } } override fun onCleared() { @@ -142,16 +151,6 @@ class MainViewModel @Inject constructor( PluviaApp.events.off(onLoggedOut) } - init { - _state.update { - it.copy( - isSteamConnected = SteamService.isConnected, - hasCrashedLastStart = PrefManager.recentlyCrashed, - launchedAppId = "", - ) - } - } - fun setTheme(value: AppTheme) { appTheme.currentTheme = value } diff --git a/app/src/main/java/app/gamenative/ui/screen/HomeScreen.kt b/app/src/main/java/app/gamenative/ui/screen/HomeScreen.kt index c9f5b6751..77ebf7882 100644 --- a/app/src/main/java/app/gamenative/ui/screen/HomeScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/HomeScreen.kt @@ -21,7 +21,6 @@ fun HomeScreen( onChat: (Long) -> Unit, onClickExit: () -> Unit, onClickPlay: (Int, Boolean) -> Unit, - onLogout: () -> Unit, onNavigateRoute: (String) -> Unit, onGoOnline: () -> Unit, isOffline: Boolean = false @@ -37,7 +36,6 @@ fun HomeScreen( HomeLibraryScreen( onClickPlay = onClickPlay, onNavigateRoute = onNavigateRoute, - onLogout = onLogout, onGoOnline = onGoOnline, isOffline = isOffline, ) @@ -57,7 +55,6 @@ private fun Preview_HomeScreenContent() { HomeScreen( onChat = {}, onClickPlay = { _, _ -> }, - onLogout = {}, onNavigateRoute = {}, onClickExit = {}, onGoOnline = {}, diff --git a/app/src/main/java/app/gamenative/ui/screen/PluviaScreen.kt b/app/src/main/java/app/gamenative/ui/screen/PluviaScreen.kt index 5fad849b8..943571da4 100644 --- a/app/src/main/java/app/gamenative/ui/screen/PluviaScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/PluviaScreen.kt @@ -4,10 +4,12 @@ package app.gamenative.ui.screen * Destinations for top level screens, excluding home screen destinations. */ sealed class PluviaScreen(val route: String) { + data object Welcome : PluviaScreen("welcome") data object LoginUser : PluviaScreen("login") data object Home : PluviaScreen("home") data object XServer : PluviaScreen("xserver") data object Settings : PluviaScreen("settings") + data object AccountManagement : PluviaScreen("accounts") data object Chat : PluviaScreen("chat/{id}") { fun route(id: Long) = "chat/$id" const val ARG_ID = "id" diff --git a/app/src/main/java/app/gamenative/ui/screen/accounts/AccountManagementScreen.kt b/app/src/main/java/app/gamenative/ui/screen/accounts/AccountManagementScreen.kt new file mode 100644 index 000000000..69796f511 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/screen/accounts/AccountManagementScreen.kt @@ -0,0 +1,242 @@ +package app.gamenative.ui.screen.accounts + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Login +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import app.gamenative.ui.component.topbar.BackButton +import app.gamenative.ui.theme.PluviaTheme +import com.alorma.compose.settings.ui.SettingsGroup +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.coil.CoilImage + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountManagementScreen( + onNavigateRoute: (String) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val snackBarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, + topBar = { + CenterAlignedTopAppBar( + title = { Text(text = "Manage Accounts") }, + navigationIcon = { + BackButton(onClick = { onBack() }) + }, + ) + }, + ) { paddingValues -> + Column( + modifier = modifier + .padding(paddingValues) + .displayCutoutPadding() + .fillMaxSize() + .verticalScroll(scrollState), + ) { + AccountsGroup(onNavigateRoute = onNavigateRoute) + } + } +} + +@Composable +private fun AccountsGroup( + onNavigateRoute: (String) -> Unit, +) { + SettingsGroup(title = { Text(text = "Accounts") }) { + SteamAccountSection(onNavigateRoute = onNavigateRoute) + // Other account sections (GOG, Epic Games, etc.) + } +} + +// Keep the existing AccountSection for backward compatibility +@Composable +fun AccountSection( + title: String, + description: String, + icon: String, + isLoggedIn: Boolean, + username: String?, + onLogin: () -> Unit, + onLogout: () -> Unit, + modifier: Modifier = Modifier, + isLoading: Boolean = false, + error: String? = null, +) { + val primaryColor = MaterialTheme.colorScheme.primary + val tertiaryColor = MaterialTheme.colorScheme.tertiary + + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.95f), + ), + border = BorderStroke(1.dp, primaryColor.copy(alpha = 0.2f)), + shape = RoundedCornerShape(16.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background( + brush = Brush.horizontalGradient( + colors = listOf(primaryColor, tertiaryColor, primaryColor), + ), + ), + ) + + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CoilImage( + imageModel = { icon }, + imageOptions = ImageOptions( + contentScale = androidx.compose.ui.layout.ContentScale.Fit, + alignment = androidx.compose.ui.Alignment.Center, + ), + modifier = Modifier.size(32.dp), + failure = { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = if (isLoggedIn) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + }, + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = if (isLoggedIn) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + + Text( + text = if (isLoggedIn && username != null) { + "Logged in as $username" + } else { + description + }, + style = MaterialTheme.typography.bodyMedium, + color = if (isLoggedIn) { + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + + // Status indicator + Icon( + imageVector = if (isLoggedIn) Icons.Default.CheckCircle else Icons.Default.Circle, + contentDescription = if (isLoggedIn) "Connected" else "Not connected", + tint = if (isLoggedIn) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(20.dp), + ) + } + + // Error message + if (error != null) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + ), + ) { + Text( + text = error, + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + } + } + + // Action button + if (isLoggedIn) { + OutlinedButton( + onClick = onLogout, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading, + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + } else { + Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null) + } + Spacer(modifier = Modifier.width(8.dp)) + Text("Sign Out") + } + } else { + Button( + onClick = onLogin, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading, + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Icon(Icons.AutoMirrored.Filled.Login, contentDescription = null) + } + Spacer(modifier = Modifier.width(8.dp)) + Text(if (isLoading) "Signing In..." else "Sign In") + } + } + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(device = "spec:width=1920px,height=1080px,dpi=440") // Odin2 Mini +@Composable +private fun AccountManagementScreenPreview() { + PluviaTheme { + AccountManagementScreen( + onNavigateRoute = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/app/gamenative/ui/screen/accounts/SteamAccountSection.kt b/app/src/main/java/app/gamenative/ui/screen/accounts/SteamAccountSection.kt new file mode 100644 index 000000000..2f9c84239 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/screen/accounts/SteamAccountSection.kt @@ -0,0 +1,29 @@ +package app.gamenative.ui.screen.accounts + +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import app.gamenative.service.SteamService +import app.gamenative.ui.screen.PluviaScreen +import kotlinx.coroutines.launch + +@Composable +fun SteamAccountSection( + onNavigateRoute: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val isSteamLoggedIn = remember { mutableStateOf(SteamService.isLoggedIn)} + + AccountSection( + title = "Steam", + description = "Access your Steam library and games", + icon = "https://store.steampowered.com/favicon.ico", + isLoggedIn = isSteamLoggedIn.value, + username = if (isSteamLoggedIn.value) "Steam User" else null, + onLogin = { onNavigateRoute(PluviaScreen.LoginUser.route) }, + onLogout = { + SteamService.logOut() + isSteamLoggedIn.value = false // Trigger a redraw + }, + modifier = modifier, + ) +} diff --git a/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt b/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt index 060039cb7..484a33d5d 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt @@ -5,29 +5,21 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.displayCutoutPadding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -37,15 +29,11 @@ import app.gamenative.data.GameSource import app.gamenative.service.SteamService import app.gamenative.ui.data.LibraryState import app.gamenative.ui.enums.AppFilter -import app.gamenative.ui.enums.Orientation -import app.gamenative.events.AndroidEvent -import app.gamenative.PluviaApp import app.gamenative.ui.internal.fakeAppInfo import app.gamenative.ui.model.LibraryViewModel import app.gamenative.ui.screen.library.components.LibraryDetailPane import app.gamenative.ui.screen.library.components.LibraryListPane import app.gamenative.ui.theme.PluviaTheme -import java.util.EnumSet @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,7 +41,6 @@ fun HomeLibraryScreen( viewModel: LibraryViewModel = hiltViewModel(), onClickPlay: (Int, Boolean) -> Unit, onNavigateRoute: (String) -> Unit, - onLogout: () -> Unit, onGoOnline: () -> Unit, isOffline: Boolean = false, ) { @@ -71,7 +58,6 @@ fun HomeLibraryScreen( onSearchQuery = viewModel::onSearchQuery, onClickPlay = onClickPlay, onNavigateRoute = onNavigateRoute, - onLogout = onLogout, onGoOnline = onGoOnline, isOffline = isOffline, ) @@ -90,7 +76,6 @@ private fun LibraryScreenContent( onSearchQuery: (String) -> Unit, onClickPlay: (Int, Boolean) -> Unit, onNavigateRoute: (String) -> Unit, - onLogout: () -> Unit, onGoOnline: () -> Unit, isOffline: Boolean = false, ) { @@ -98,14 +83,16 @@ private fun LibraryScreenContent( BackHandler(selectedAppId != null) { selectedAppId = null } val safePaddingModifier = - if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { Modifier.displayCutoutPadding() - else + } else { Modifier + } Box( Modifier.background(MaterialTheme.colorScheme.background) - .then(safePaddingModifier)) { + .then(safePaddingModifier), + ) { if (selectedAppId == null) { LibraryListPane( state = state, @@ -117,7 +104,6 @@ private fun LibraryScreenContent( onIsSearching = onIsSearching, onSearchQuery = onSearchQuery, onNavigateRoute = onNavigateRoute, - onLogout = onLogout, onNavigate = { appId -> selectedAppId = appId }, onGoOnline = onGoOnline, isOffline = isOffline, @@ -191,7 +177,6 @@ private fun Preview_LibraryScreenContent() { }, onClickPlay = { _, _ -> }, onNavigateRoute = {}, - onLogout = {}, onGoOnline = {}, ) } diff --git a/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryDetailPane.kt b/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryDetailPane.kt index 825d1d79e..599cd4211 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryDetailPane.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryDetailPane.kt @@ -34,7 +34,7 @@ internal fun LibraryDetailPane( LibraryState( appInfoList = emptyList(), // Use the same default filter as in PrefManager (GAME) - appInfoSortType = EnumSet.of(AppFilter.GAME) + appInfoSortType = EnumSet.of(AppFilter.GAME), ) } @@ -46,10 +46,9 @@ internal fun LibraryDetailPane( onPageChange = {}, onModalBottomSheet = {}, onIsSearching = {}, - onLogout = {}, - onNavigate = {}, onSearchQuery = {}, onNavigateRoute = {}, + onNavigate = {}, onGoOnline = {}, ) } else { diff --git a/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt b/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt index 90ed9f592..5918b18d6 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListPane.kt @@ -9,12 +9,16 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -24,13 +28,16 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -39,22 +46,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import app.gamenative.PrefManager import app.gamenative.data.LibraryItem +import app.gamenative.service.DownloadService +import app.gamenative.ui.component.topbar.AccountButton import app.gamenative.ui.data.LibraryState import app.gamenative.ui.enums.AppFilter import app.gamenative.ui.internal.fakeAppInfo -import app.gamenative.service.DownloadService import app.gamenative.ui.theme.PluviaTheme -import app.gamenative.ui.component.topbar.AccountButton -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.snapshotFlow -import app.gamenative.PrefManager import app.gamenative.utils.DeviceUtils +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.distinctUntilChanged import app.gamenative.data.GameSource @@ -69,7 +70,6 @@ internal fun LibraryListPane( onModalBottomSheet: (Boolean) -> Unit, onPageChange: (Int) -> Unit, onIsSearching: (Boolean) -> Unit, - onLogout: () -> Unit, onNavigate: (String) -> Unit, onSearchQuery: (String) -> Unit, onNavigateRoute: (String) -> Unit, @@ -89,31 +89,32 @@ internal fun LibraryListPane( .filterNotNull() .distinctUntilChanged() .collect { lastVisibleIndex -> - if (lastVisibleIndex >= state.appInfoList.lastIndex - && state.appInfoList.size < state.totalAppsInFilter) { + if (lastVisibleIndex >= state.appInfoList.lastIndex && + state.appInfoList.size < state.totalAppsInFilter + ) { onPageChange(1) } } } Scaffold( - snackbarHost = { SnackbarHost(snackBarHost) } + snackbarHost = { SnackbarHost(snackBarHost) }, ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) + .padding(top = paddingValues.calculateTopPadding()), ) { // Modern Header with gradient Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(16.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween + horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, ) { Column { Text( @@ -123,15 +124,15 @@ internal fun LibraryListPane( brush = Brush.horizontalGradient( colors = listOf( MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.tertiary - ) - ) - ) + MaterialTheme.colorScheme.tertiary, + ), + ), + ), ) Text( text = "${state.totalAppsInFilter} games • $installedCount installed", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -139,7 +140,7 @@ internal fun LibraryListPane( Box( modifier = Modifier .weight(1f) - .padding(horizontal = 30.dp) + .padding(horizontal = 30.dp), ) { LibrarySearchBar( state = state, @@ -154,11 +155,10 @@ internal fun LibraryListPane( modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) - .padding(8.dp) + .padding(8.dp), ) { AccountButton( onNavigateRoute = onNavigateRoute, - onLogout = onLogout, onGoOnline = onGoOnline, isOffline = isOffline, ) @@ -166,12 +166,12 @@ internal fun LibraryListPane( } } - if (! isViewWide) { + if (!isViewWide) { // Search bar Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 12.dp) + .padding(horizontal = 20.dp, vertical = 12.dp), ) { LibrarySearchBar( state = state, @@ -191,14 +191,14 @@ internal fun LibraryListPane( contentPadding = PaddingValues( start = 20.dp, end = 20.dp, - bottom = 72.dp + bottom = 72.dp, ), ) { items(items = state.appInfoList, key = { it.index }) { item -> AppItem( modifier = Modifier.animateItem(), appInfo = item, - onClick = { onNavigate(item.appId) } + onClick = { onNavigate(item.appId) }, ) if (item.index < state.appInfoList.lastIndex) { HorizontalDivider() @@ -210,7 +210,7 @@ internal fun LibraryListPane( modifier = Modifier .fillMaxWidth() .padding(16.dp), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } @@ -229,7 +229,7 @@ internal fun LibraryListPane( contentColor = MaterialTheme.colorScheme.onPrimary, modifier = Modifier .align(Alignment.BottomEnd) - .padding(24.dp) + .padding(24.dp), ) } @@ -294,7 +294,6 @@ private fun Preview_LibraryListPane() { onIsSearching = { }, onSearchQuery = { }, onNavigateRoute = { }, - onLogout = { }, onNavigate = { }, onGoOnline = { }, ) diff --git a/app/src/main/java/app/gamenative/ui/screen/welcome/WelcomeScreen.kt b/app/src/main/java/app/gamenative/ui/screen/welcome/WelcomeScreen.kt new file mode 100644 index 000000000..65410d86e --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/screen/welcome/WelcomeScreen.kt @@ -0,0 +1,154 @@ +package app.gamenative.ui.screen.welcome + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.gamenative.R +import app.gamenative.ui.theme.PluviaTheme + +@Composable +fun WelcomeScreen( + onSetupAccounts: () -> Unit, + onSkipSignIn: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .verticalScroll(scrollState) + .padding(24.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), + modifier = Modifier.fillMaxWidth(), + ) { + // Logo and branding - using the exact styling you provided + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Brand name with the exact styling you specified + Text( + text = "GameNative", + style = MaterialTheme.typography.headlineLarge.copy( + fontWeight = FontWeight.Bold, + brush = Brush.horizontalGradient( + colors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.tertiary, + ), + ), + ), + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Welcome content + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "Get Started", + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + + Text( + text = "Welcome to the ultimate PC gaming experience on Android. Let's set up your accounts to get you gaming.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 24.sp, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Action buttons + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth(), + ) { + // Set up accounts button (primary) + Button( + onClick = onSetupAccounts, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + contentPadding = PaddingValues(vertical = 16.dp), + ) { + Text( + text = "Set Up Accounts", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + } + + // Skip sign in button (secondary) + OutlinedButton( + onClick = onSkipSignIn, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f), + ), + border = ButtonDefaults.outlinedButtonBorder.copy( + brush = Brush.linearGradient( + colors = listOf( + MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), + MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + ), + ), + ), + contentPadding = PaddingValues(vertical = 16.dp), + ) { + Text( + text = "Skip Sign In", + style = MaterialTheme.typography.titleMedium, + ) + } + } + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(device = "spec:width=1920px,height=1080px,dpi=440") // Odin2 Mini +@Composable +private fun WelcomeScreenPreview() { + PluviaTheme { + WelcomeScreen( + onSetupAccounts = {}, + onSkipSignIn = {}, + ) + } +}