diff --git a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt index 77a34452..c7626edc 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -415,6 +415,15 @@ fun AppNavigation( ) { MemoryLaneScreen(onNavigate = navigateRoute) } + composable( + route = AppRoute.MEMORY_LANE_CAPSULE, + arguments = listOf(navArgument("capsuleId") { type = NavType.StringType }) + ) { + MemoryLaneScreen( + onNavigate = navigateRoute, + startingCapsuleId = it.arguments?.getString("capsuleId") + ) + } composable(route = AppRoute.WAITING_FOR_PARTNER) { WaitingForPartnerScreen( onNavigate = navigateRoute diff --git a/app/src/main/java/app/closer/core/navigation/AppRoute.kt b/app/src/main/java/app/closer/core/navigation/AppRoute.kt index 1d61eceb..c7b08229 100644 --- a/app/src/main/java/app/closer/core/navigation/AppRoute.kt +++ b/app/src/main/java/app/closer/core/navigation/AppRoute.kt @@ -50,6 +50,7 @@ object AppRoute { const val DESIRE_SYNC = "desire_sync" const val CONNECTION_CHALLENGES = "connection_challenges" const val MEMORY_LANE = "memory_lane" + const val MEMORY_LANE_CAPSULE = "memory_lane_capsule/{capsuleId}" const val WAITING_FOR_PARTNER = "waiting_for_partner" const val RECOVERY = "recovery" const val ENCRYPTION_UPGRADE = "encryption_upgrade" @@ -58,6 +59,8 @@ object AppRoute { fun pairingSuccess(coupleId: String) = "pairing_success/$coupleId" + fun memorylaneDetail(capsuleId: String): String = "memory_lane_capsule/${capsuleId.asRouteArg()}" + // Question thread: coupleId and questionId are required; prevId and nextId are optional. const val QUESTION_THREAD = "question_thread/{coupleId}/{questionId}?prevId={prevId}&nextId={nextId}" @@ -116,6 +119,7 @@ object AppRoute { Definition(DESIRE_SYNC, "Desire Sync", "play"), Definition(CONNECTION_CHALLENGES, "Connection Challenges", "play"), Definition(MEMORY_LANE, "Memory Lane", "play"), + Definition(MEMORY_LANE_CAPSULE, "Memory Lane", "play"), Definition(WAITING_FOR_PARTNER, "Waiting for Partner", "play"), Definition(RECOVERY, "Unlock Answers", "security"), Definition(ENCRYPTION_UPGRADE, "Secure Answers", "security"), @@ -175,6 +179,7 @@ object AppRoute { SECURITY, CONNECTION_CHALLENGES, MEMORY_LANE, + MEMORY_LANE_CAPSULE, WAITING_FOR_PARTNER, YOUR_PROGRESS ) diff --git a/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt b/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt index 90e20f36..8b94a3dc 100644 --- a/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt +++ b/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt @@ -748,6 +748,24 @@ private fun ChallengesActiveScreen( } } } + item { + Button( + onClick = onViewHistory, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 54.dp), + shape = RoundedCornerShape(18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CloserPalette.PurpleDeep + ) + ) { + Text( + text = "View game history", + style = MaterialTheme.typography.labelLarge, + color = Color.White + ) + } + } } if (!isComplete) { diff --git a/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt b/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt index 89b6cd7a..715959aa 100644 --- a/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt +++ b/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt @@ -58,6 +58,7 @@ 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.ui.Alignment import androidx.compose.ui.Modifier @@ -328,11 +329,25 @@ class MemoryLaneViewModel @Inject constructor( @Composable fun MemoryLaneScreen( onNavigate: (String) -> Unit = {}, + startingCapsuleId: String? = null, viewModel: MemoryLaneViewModel = hiltViewModel() ) { val state by viewModel.uiState.collectAsState() val snackbarHostState = remember { SnackbarHostState() } + // Deep-link: open a specific capsule once the list is loaded. The flag prevents + // re-opening if the user navigates back to the list and capsules update again. + val deepLinkHandled = remember { mutableStateOf(false) } + LaunchedEffect(startingCapsuleId, state.capsules) { + if (startingCapsuleId == null || deepLinkHandled.value) return@LaunchedEffect + if (state.capsules.isEmpty()) return@LaunchedEffect + val capsule = state.capsules.firstOrNull { it.id == startingCapsuleId } + if (capsule != null) { + deepLinkHandled.value = true + viewModel.openDetail(capsule) + } + } + LaunchedEffect(state.navigateTo) { state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() } } diff --git a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt index 17ad63b5..2553dc58 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -108,7 +109,7 @@ fun GameHistoryScreen( GameHistoryLockedCard(onUnlock = { onNavigate(AppRoute.PAYWALL) }) } state.isLoading -> item { LoadingState(message = "Loading your gamesโ€ฆ") } - state.error != null -> item { + !hasAny && state.error != null -> item { ErrorState( message = state.error!!, onRetry = viewModel::load @@ -131,17 +132,45 @@ fun GameHistoryScreen( onClick = sessionReplayRoute(session)?.let { route -> { onNavigate(route) } } ) } + } else if (state.error != null) { + item { + Card( + modifier = Modifier.fillMaxWidth(), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Couldn't load game sessions", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.weight(1f) + ) + TextButton(onClick = viewModel::load) { + Text("Retry", color = MaterialTheme.colorScheme.error) + } + } + } + } } if (state.completedChallenges.isNotEmpty()) { item { HistorySectionHeader("Completed Challenges") } items(state.completedChallenges, key = { it.challengeId }) { entry -> - ChallengeHistoryCard(entry) + ChallengeHistoryCard(entry, onClick = { onNavigate(AppRoute.CONNECTION_CHALLENGES) }) } } if (state.unlockedCapsules.isNotEmpty()) { item { HistorySectionHeader("Memory Lane") } items(state.unlockedCapsules, key = { it.id }) { capsule -> - CapsuleHistoryCard(capsule) + CapsuleHistoryCard(capsule, onClick = { onNavigate(AppRoute.memorylaneDetail(capsule.id)) }) } } } @@ -167,6 +196,7 @@ private fun WheelSessionCard(session: QuestionSession, onClick: (() -> Unit)?) { horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically ) { + Text(sessionEmoji(session), style = MaterialTheme.typography.headlineSmall) Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { Text( text = sessionTitle(session), @@ -203,8 +233,10 @@ private fun HistorySectionHeader(title: String) { } @Composable -private fun ChallengeHistoryCard(entry: CompletedChallengeEntry) { +private fun ChallengeHistoryCard(entry: CompletedChallengeEntry, onClick: (() -> Unit)? = null) { Card( + onClick = onClick ?: {}, + enabled = onClick != null, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)), @@ -241,8 +273,10 @@ private fun ChallengeHistoryCard(entry: CompletedChallengeEntry) { } @Composable -private fun CapsuleHistoryCard(capsule: TimeCapsule) { +private fun CapsuleHistoryCard(capsule: TimeCapsule, onClick: (() -> Unit)? = null) { Card( + onClick = onClick ?: {}, + enabled = onClick != null, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)), @@ -288,6 +322,14 @@ private fun sessionTitle(session: QuestionSession): String = when (session.gameT else -> "Game Session" } +private fun sessionEmoji(session: QuestionSession): String = when (session.gameType) { + GameType.THIS_OR_THAT -> "๐Ÿ”€" + GameType.HOW_WELL -> "๐ŸŽฏ" + GameType.DESIRE_SYNC -> "๐Ÿ’‹" + GameType.WHEEL -> "๐ŸŽก" + else -> "๐ŸŽฎ" +} + private fun sessionReplayRoute(session: QuestionSession): String? = when (session.gameType) { GameType.THIS_OR_THAT -> AppRoute.thisOrThatReplay(session.id) GameType.HOW_WELL -> AppRoute.howWellReplay(session.id)