package dev.privacyllc.period.navigation import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Insights import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Today import androidx.compose.material.icons.filled.Circle import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController import dev.privacyllc.period.R import dev.privacyllc.period.designsystem.PeriodTheme import dev.privacyllc.period.feature.onboarding.OnboardingScreen import dev.privacyllc.period.feature.today.TodayScreen /** * The four tabs from docs/planning/PRODUCT_PLAN.md ยง20. * * Four and only four. Every proposal to add a fifth is answered by * docs/planning/PROJECT_PLAN.md's "deliberately not" list. */ enum class PeriodDestination( val route: String, @param:StringRes val labelRes: Int, val icon: ImageVector, ) { TODAY("today", R.string.tab_today, Icons.Filled.Today), CALENDAR("calendar", R.string.tab_calendar, Icons.Filled.CalendarMonth), INSIGHTS("insights", R.string.tab_insights, Icons.Filled.Insights), SETTINGS("settings", R.string.tab_settings, Icons.Filled.Settings), } /** * The root: onboarding, then the app. * * The decision comes from `UserPreferences.onboardingCompleted` and from * nowhere else. A second source โ€” "does any period exist?" โ€” would look * equivalent and is not: somebody who deletes all their data has not asked to be * onboarded again, and somebody who abandoned onboarding halfway has. */ @Composable fun PeriodRoot(viewModel: RootViewModel = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() when (state) { RootState.Loading -> Unit // one frame; a spinner here flashes and reads as jank RootState.Onboarding -> OnboardingScreen(onFinished = viewModel::onboardingFinished) RootState.Ready -> PeriodApp() } } @Composable fun PeriodApp() { val navController = rememberNavController() val backStackEntry by navController.currentBackStackEntryAsState() val currentDestination = backStackEntry?.destination Scaffold( bottomBar = { NavigationBar { PeriodDestination.entries.forEach { destination -> val selected = currentDestination?.hierarchy?.any { it.route == destination.route } == true NavigationBarItem( selected = selected, onClick = { navController.navigate(destination.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } }, icon = { Icon(destination.icon, contentDescription = null) }, label = { Text(stringResource(destination.labelRes)) }, ) } } }, ) { innerPadding -> NavHost( navController = navController, startDestination = PeriodDestination.TODAY.route, modifier = Modifier.padding(innerPadding), ) { composable(PeriodDestination.TODAY.route) { TodayScreen() } // Calendar, Insights and Settings arrive in Batches 03 and 06. They // say "not built yet" rather than showing a convincing mock. PeriodDestination.entries .filter { it != PeriodDestination.TODAY } .forEach { destination -> composable(destination.route) { PlaceholderScreen(stringResource(destination.labelRes)) } } } } } /** * Skeleton only. Each of these is replaced by its real screen in Batch 03, and * saying "not built yet" on the screen is deliberate โ€” a convincing mock is how * a screen comes to be believed finished. */ @Composable private fun PlaceholderScreen(title: String) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( Icons.Filled.Circle, contentDescription = null, tint = MaterialTheme.colorScheme.primaryContainer, ) Text( text = title, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground, ) Text( text = "Not built yet", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, ) } } @Preview(showBackground = true) @Composable private fun PlaceholderPreview() { PeriodTheme { PlaceholderScreen("Calendar") } }