diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt index a0d652c4..423ad06f 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/MainNavigationWrapper.kt @@ -1,6 +1,7 @@ package com.cornellappdev.uplift.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -45,6 +46,7 @@ import com.cornellappdev.uplift.ui.screens.gyms.GymDetailScreen import com.cornellappdev.uplift.ui.screens.gyms.HomeScreen import com.cornellappdev.uplift.ui.screens.onboarding.ProfileCreationScreen import com.cornellappdev.uplift.ui.screens.onboarding.SignInPromptScreen +import com.cornellappdev.uplift.ui.screens.profile.GuestProfileScreen import com.cornellappdev.uplift.ui.screens.profile.ProfileScreen import com.cornellappdev.uplift.ui.screens.profile.SettingsScreen import com.cornellappdev.uplift.ui.screens.profile.WorkoutHistoryScreen @@ -82,6 +84,14 @@ fun MainNavigationWrapper( rootNavigationViewModel: RootNavigationViewModel = hiltViewModel(), ) { val rootNavigationUiState = rootNavigationViewModel.collectUiStateValue() + + // Wait for the saved skip-login preference before creating the graph or consuming navigation + // events. Otherwise a returning guest briefly sees Onboarding before being sent to Home. + if (!rootNavigationUiState.isStartupReady) { + Box(Modifier.fillMaxSize().background(Color.White)) + return + } + val startDestination = rootNavigationUiState.startDestination val navController = rememberNavController() @@ -118,8 +128,14 @@ fun MainNavigationWrapper( //TODO: Try to consolidate launched effects into one with consumeIn function that takes in coroutine scope LaunchedEffect(rootNavigationUiState.navEvent) { - rootNavigationUiState.navEvent?.consumeSuspend { - navController.navigate(it) + rootNavigationUiState.navEvent?.consumeSuspend { route -> + navController.navigate(route) { + if (route == UpliftRootRoute.Home) { + // Finish skip/login/onboarding so pressing back won't bring the user back into the onboarding flow. + popUpTo(0) + launchSingleTop = true + } + } } } LaunchedEffect(rootNavigationUiState.popBackStack) { @@ -258,7 +274,11 @@ fun MainNavigationWrapper( CapacityReminderScreen() } composable { - ProfileScreen() + if (isLoggedIn) { + ProfileScreen(loadingShimmer = shimmer) + } else { + GuestProfileScreen() + } } composable { MainReminderScreen() diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/LoadingPlaceholder.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/LoadingPlaceholder.kt new file mode 100644 index 00000000..3e1aa01a --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/LoadingPlaceholder.kt @@ -0,0 +1,29 @@ +package com.cornellappdev.uplift.ui.components.general + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import com.cornellappdev.uplift.util.GRAY01 +import com.valentinilk.shimmer.Shimmer +import com.valentinilk.shimmer.shimmer + +/** + * Shared rendering for Home and Profile skeletons, extracted from Home's LoadingBlob. + * Keep the existing gray Surface and apply shimmer to each placeholder; the caller supplies + * the shared window-bounded animation so all placeholders use the same theme and timing. + */ +@Composable +fun LoadingPlaceholder( + shimmer: Shimmer, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(6.dp) +) { + Surface( + color = GRAY01, + modifier = modifier.shimmer(shimmer), + shape = shape + ) {} +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftButton.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftButton.kt index 5663750e..ae93f2c7 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftButton.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/general/UpliftButton.kt @@ -90,7 +90,7 @@ fun UpliftButton( fontSize = fontSize.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, - modifier = modifier.wrapContentSize() + modifier = Modifier.wrapContentSize() ) } } @@ -100,4 +100,4 @@ fun UpliftButton( @Composable fun UpliftButtonPreview() { UpliftButton(onClick = { /*TODO*/ }) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/components/onboarding/auth/LogInButton.kt b/app/src/main/java/com/cornellappdev/uplift/ui/components/onboarding/auth/LogInButton.kt index a2cf5b5c..5f5bc224 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/components/onboarding/auth/LogInButton.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/components/onboarding/auth/LogInButton.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.credentials.Credential @@ -16,7 +17,10 @@ import com.cornellappdev.uplift.ui.components.general.UpliftButton import kotlinx.coroutines.launch @Composable -fun LogInButton(onRequestResult: (Credential) -> Unit) { +fun LogInButton( + onRequestResult: (Credential) -> Unit, + modifier: Modifier = Modifier +) { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() UpliftButton( @@ -32,7 +36,8 @@ fun LogInButton(onRequestResult: (Credential) -> Unit) { width = 144.dp, height = 44.dp, fontSize = 16f, - elevation = 2.dp + elevation = 2.dp, + modifier = modifier ) } @@ -60,4 +65,4 @@ private suspend fun launchCredentialManagerButtonUI( Log.e("CredentialManager", e.message.orEmpty(), e) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/gyms/subscreens/MainLoading.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/gyms/subscreens/MainLoading.kt index 51c1b955..30b53c81 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/gyms/subscreens/MainLoading.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/gyms/subscreens/MainLoading.kt @@ -15,16 +15,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.uplift.ui.components.general.LoadingTopBar -import com.cornellappdev.uplift.util.GRAY01 +import com.cornellappdev.uplift.ui.components.general.LoadingPlaceholder import com.valentinilk.shimmer.Shimmer -import com.valentinilk.shimmer.shimmer @OptIn(ExperimentalFoundationApi::class) @Composable @@ -140,14 +138,13 @@ private fun LoadingBlob( cornerRadius: Dp, paddingValues: PaddingValues = PaddingValues() ) { - Surface( - color = GRAY01, + LoadingPlaceholder( + shimmer = shimmerInstance, modifier = Modifier .padding(paddingValues) - .size(width = width, height = height) - .shimmer(shimmerInstance), + .size(width = width, height = height), shape = RoundedCornerShape(cornerRadius) - ) {} + ) } /** @@ -161,13 +158,12 @@ private fun LoadingBlob( cornerRadius: Dp, paddingValues: PaddingValues = PaddingValues() ) { - Surface( - color = GRAY01, + LoadingPlaceholder( + shimmer = shimmerInstance, modifier = Modifier .padding(paddingValues) .height(height) - .shimmer(shimmerInstance) .fillMaxWidth(), shape = RoundedCornerShape(cornerRadius) - ) {} + ) } diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/GuestProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/GuestProfileScreen.kt new file mode 100644 index 00000000..be96c93d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/GuestProfileScreen.kt @@ -0,0 +1,144 @@ +package com.cornellappdev.uplift.ui.screens.profile + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +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.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +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 androidx.credentials.Credential +import androidx.hilt.navigation.compose.hiltViewModel +import com.cornellappdev.uplift.R +import com.cornellappdev.uplift.ui.components.onboarding.auth.LogInButton +import com.cornellappdev.uplift.ui.viewmodels.onboarding.LoginViewModel +import com.cornellappdev.uplift.util.LIGHT_YELLOW +import com.cornellappdev.uplift.util.PRIMARY_BLACK +import com.cornellappdev.uplift.util.montserratFamily + +@Composable +fun GuestProfileScreen( + loginViewModel: LoginViewModel = hiltViewModel(), +) { + GuestProfileScreenContent(loginViewModel::onSignInWithGoogle) +} + +@Composable +private fun GuestProfileScreenContent(onSignIn: (Credential) -> Unit) { + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .clipToBounds() + ) { + val headerScale = (maxHeight.value / 769f).coerceIn(0.65f, 1.2f) + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(375.dp * headerScale) + .drawBehind { + drawCircle( + color = LIGHT_YELLOW, + radius = 353.5.dp.toPx() * headerScale, + center = Offset(size.width * 101.5f / 393f, 4.5.dp.toPx() * headerScale) + ) + }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.height(136.dp * headerScale)) + Image( + painter = painterResource(R.drawable.ic_main_logo), + contentDescription = "Uplift logo", + modifier = Modifier + .width(207.dp * headerScale) + .height(183.dp * headerScale) + ) + } + Spacer(Modifier.height(24.dp)) + Text( + text = "Create your Uplift profile.", + modifier = Modifier.padding(horizontal = 16.dp), + fontFamily = montserratFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 30.sp, + color = PRIMARY_BLACK, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(24.dp)) + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + GuestProfileBenefit(R.drawable.guest_profile_goal, "Create fitness goals") + GuestProfileBenefit(R.drawable.gym_simple, "Track fitness progress") + GuestProfileBenefit(R.drawable.history, "View workout history") + } + Spacer(Modifier.height(48.dp)) + LogInButton( + onRequestResult = onSignIn, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + Spacer(Modifier.height(32.dp)) + } + } +} + +@Composable +private fun GuestProfileBenefit(@DrawableRes icon: Int, text: String) { + Row( + modifier = Modifier + .width(240.dp) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + painter = painterResource(icon), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Text( + text = text, + fontFamily = montserratFamily, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 16.sp, + color = PRIMARY_BLACK.copy(alpha = 0.9f) + ) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 769) +@Composable +private fun GuestProfilePreview() { + GuestProfileScreenContent {} +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileLoading.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileLoading.kt new file mode 100644 index 00000000..525770c1 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileLoading.kt @@ -0,0 +1,97 @@ +package com.cornellappdev.uplift.ui.screens.profile + +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.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.unit.dp +import com.cornellappdev.uplift.ui.components.general.LoadingPlaceholder +import com.valentinilk.shimmer.Shimmer + +@Composable +internal fun ProfileLoading(shimmer: Shimmer) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, top = 24.dp) + .clearAndSetSemantics { + contentDescription = "Loading profile" + progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + LoadingPlaceholder(shimmer, Modifier.size(98.dp), CircleShape) + + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) { + LoadingPlaceholder(shimmer, Modifier.fillMaxWidth(0.8f).height(24.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(36.dp)) { + repeat(2) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + LoadingPlaceholder(shimmer, Modifier.width(24.dp).height(16.dp)) + LoadingPlaceholder(shimmer, Modifier.width(60.dp).height(12.dp)) + } + } + } + } + } + + Spacer(Modifier.height(12.dp)) + LoadingPlaceholder(shimmer, Modifier.width(100.dp).height(24.dp)) + Spacer(Modifier.height(12.dp)) + LoadingPlaceholder(shimmer, Modifier.align(Alignment.CenterHorizontally).width(250.dp).height(132.dp)) + Spacer(Modifier.height(16.dp)) + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + repeat(7) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + LoadingPlaceholder(shimmer, Modifier.width(20.dp).height(12.dp)) + Spacer(Modifier.height(4.dp)) + LoadingPlaceholder(shimmer, Modifier.size(24.dp), CircleShape) + Spacer(Modifier.height(4.dp)) + LoadingPlaceholder(shimmer, Modifier.width(16.dp).height(12.dp)) + } + } + } + + Spacer(Modifier.height(24.dp)) + + Column(Modifier.fillMaxWidth().weight(1f)) { + LoadingPlaceholder(shimmer, Modifier.width(180.dp).height(24.dp)) + Spacer(Modifier.height(12.dp)) + Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + repeat(3) { + Row( + modifier = Modifier.fillMaxWidth().height(60.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + LoadingPlaceholder(shimmer, Modifier.width(100.dp).height(12.dp)) + LoadingPlaceholder(shimmer, Modifier.width(160.dp).height(12.dp)) + } + LoadingPlaceholder(shimmer, Modifier.width(48.dp).height(12.dp)) + } + } + } + } + } +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt index de192439..69573915 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/screens/profile/ProfileScreen.kt @@ -1,8 +1,7 @@ package com.cornellappdev.uplift.ui.screens.profile -import android.annotation.SuppressLint -import android.net.Uri import androidx.compose.foundation.layout.Arrangement +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -16,7 +15,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar 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.Modifier @@ -28,36 +26,47 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.compose.viewModel import com.cornellappdev.uplift.R import com.cornellappdev.uplift.ui.components.profile.workouts.GoalsSection import com.cornellappdev.uplift.ui.components.profile.workouts.HistoryItem import com.cornellappdev.uplift.ui.components.profile.workouts.HistorySection import com.cornellappdev.uplift.ui.components.profile.ProfileHeaderSection -import com.cornellappdev.uplift.ui.components.profile.workouts.ReminderItem +import com.cornellappdev.uplift.ui.screens.gyms.subscreens.MainError import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileUiState import com.cornellappdev.uplift.ui.viewmodels.profile.ProfileViewModel import com.cornellappdev.uplift.util.GRAY01 import com.cornellappdev.uplift.util.montserratFamily +import com.valentinilk.shimmer.Shimmer +import com.valentinilk.shimmer.ShimmerBounds +import com.valentinilk.shimmer.rememberShimmer -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ProfileScreen( + loadingShimmer: Shimmer, viewModel: ProfileViewModel = hiltViewModel() ) { val uiState by viewModel.uiStateFlow.collectAsState() - ProfileScreenContent(uiState,viewModel::toSettings,viewModel::toGoals, viewModel::toHistory) - + ProfileScreenContent( + uiState = uiState, + toSettings = viewModel::toSettings, + toGoals = viewModel::toGoals, + toHistory = viewModel::toHistory, + onRetry = viewModel::reload, + loadingShimmer = loadingShimmer + ) } +private enum class ProfileContentState { Loading, Error, Loaded } + @Composable private fun ProfileScreenContent( uiState: ProfileUiState, toSettings: () -> Unit, toGoals: () -> Unit, - toHistory: () -> Unit + toHistory: () -> Unit, + onRetry: () -> Unit, + loadingShimmer: Shimmer ) { Scaffold( containerColor = Color.White, @@ -65,34 +74,53 @@ private fun ProfileScreenContent( ProfileScreenTopBar(navigateToSettings = toSettings) } ) { innerPadding -> - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .fillMaxSize() - .padding( - top = innerPadding.calculateTopPadding() + 24.dp, - start = 16.dp, - end = 16.dp, - ) - ) { - ProfileHeaderSection( - name = uiState.name, - gymDays = uiState.totalGymDays, - streaks = uiState.activeStreak, - profilePictureUri = uiState.profileImage, - onPhotoSelected = {}, - netId = uiState.netId - ) - WorkoutsSectionContent( - workoutsCompleted = uiState.workoutsCompleted, - workoutGoal = uiState.workoutGoal, - daysOfMonth = uiState.daysOfMonth, - completedDays = uiState.completedDays, - historyItems = uiState.historyItems, - navigateToGoalsSection = toGoals, - navigateToHistorySection = toHistory - ) + // Render placeholders before data arrives instead of flashing empty names and zero stats. + // Keep the toolbar stable and crossfade on loading/error/content changes. + val contentState = when { + uiState.loading -> ProfileContentState.Loading + uiState.error -> ProfileContentState.Error + else -> ProfileContentState.Loaded + } + Crossfade( + targetState = contentState, + modifier = Modifier.fillMaxSize().padding(innerPadding), + label = "Profile" + ) { state -> + when (state) { + ProfileContentState.Loading -> ProfileLoading(loadingShimmer) + // Reuse the existing retry UI so a failed request does not look like an empty profile. + ProfileContentState.Error -> MainError(reload = onRetry) + ProfileContentState.Loaded -> + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxSize() + .padding( + top = 24.dp, + start = 16.dp, + end = 16.dp, + ) + ) { + ProfileHeaderSection( + name = uiState.name, + gymDays = uiState.totalGymDays, + streaks = uiState.activeStreak, + profilePictureUri = uiState.profileImage, + onPhotoSelected = {}, + netId = uiState.netId + ) + WorkoutsSectionContent( + workoutsCompleted = uiState.workoutsCompleted, + workoutGoal = uiState.workoutGoal, + daysOfMonth = uiState.daysOfMonth, + completedDays = uiState.completedDays, + historyItems = uiState.historyItems, + navigateToGoalsSection = toGoals, + navigateToHistorySection = toHistory + ) + } + } } } } @@ -190,6 +218,8 @@ private fun ProfileScreenContentPreview() { ), {}, {}, - {} + {}, + onRetry = {}, + loadingShimmer = rememberShimmer(ShimmerBounds.Window) ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt index aca57c5e..9c26336d 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/nav/RootNavigationViewModel.kt @@ -32,11 +32,26 @@ class RootNavigationViewModel @Inject constructor( ) { data class RootNavigationUiState( val isLoggedIn: Boolean = false, + // The initial destination is temporary until the saved skip preference has been read. + val isStartupReady: Boolean = false, val navEvent: UIEvent? = null, val popBackStack: UIEvent? = null, val navigateUp: UIEvent? = null, val startDestination: UpliftRootRoute = if (ONBOARDING_FLAG) UpliftRootRoute.Onboarding else UpliftRootRoute.Home - ) + ) { + // Determines the guest-to-authenticated transition + internal fun withSession(loggedIn: Boolean, destination: UpliftRootRoute): RootNavigationUiState { + // On startup, NavHost opens the resolved destination directly. + val shouldNavigate = isStartupReady && + (destination != startDestination || loggedIn != isLoggedIn) + return copy( + isLoggedIn = loggedIn, + isStartupReady = true, + startDestination = destination, + navEvent = if (shouldNavigate) UIEvent(destination) else navEvent + ) + } + } init { @@ -60,25 +75,16 @@ class RootNavigationViewModel @Inject constructor( viewModelScope.launch { sessionManager.isLoggedIn.collect { loggedIn -> - applyMutation { - copy(isLoggedIn = loggedIn) - } - val hasSkipped = userInfoRepository.getSkipFromDataStore() val shouldShowHome = loggedIn || hasSkipped || !ONBOARDING_FLAG val newRoute = if (shouldShowHome) UpliftRootRoute.Home else UpliftRootRoute.Onboarding applyMutation { - // Only attach a navEvent if we are actually changing the destination compared to what was set during initialization. - val shouldNav = newRoute != startDestination || loggedIn != isLoggedIn - - copy( - isLoggedIn = loggedIn, - startDestination = newRoute, - navEvent = if (shouldNav) UIEvent(newRoute) else navEvent - ) + // Compare against the previous session before updating it: guest login + // must finish onboarding even when Home is already the start destination. + withSession(loggedIn, newRoute) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt index 76b15adb..93d4c91c 100644 --- a/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/cornellappdev/uplift/ui/viewmodels/profile/ProfileViewModel.kt @@ -48,11 +48,12 @@ data class ProfileUiState( val workoutDates: Map> = emptyMap() ) +// Start in loading before the reload coroutine runs, preventing an initial empty-profile frame. @HiltViewModel class ProfileViewModel @Inject constructor( private val profileRepository: ProfileRepository, private val rootNavigationRepository: RootNavigationRepository, -) : UpliftViewModel(ProfileUiState()) { +) : UpliftViewModel(ProfileUiState(loading = true)) { private var loadingJob: Job? = null diff --git a/app/src/main/res/drawable/guest_profile_goal.xml b/app/src/main/res/drawable/guest_profile_goal.xml new file mode 100644 index 00000000..39a85b58 --- /dev/null +++ b/app/src/main/res/drawable/guest_profile_goal.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_main_logo.png b/app/src/main/res/drawable/ic_main_logo.png deleted file mode 100644 index 58c2ea01..00000000 Binary files a/app/src/main/res/drawable/ic_main_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_main_logo.xml b/app/src/main/res/drawable/ic_main_logo.xml new file mode 100644 index 00000000..29112e20 --- /dev/null +++ b/app/src/main/res/drawable/ic_main_logo.xml @@ -0,0 +1,14 @@ + + + +