diff --git a/app/src/commonMain/kotlin/id/gdg/app/AppContent.kt b/app/src/commonMain/kotlin/id/gdg/app/App.kt similarity index 60% rename from app/src/commonMain/kotlin/id/gdg/app/AppContent.kt rename to app/src/commonMain/kotlin/id/gdg/app/App.kt index ebf17cf..94dd994 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/AppContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/App.kt @@ -14,6 +14,7 @@ import androidx.navigation.navArgument import id.gdg.app.di.ViewModelFactory import id.gdg.app.ui.AppEvent import id.gdg.app.ui.AppRouter +import id.gdg.app.ui.ScreenScaffold import id.gdg.app.ui.screen.EventDetailScreen import id.gdg.app.ui.screen.MainScreen import id.gdg.app.ui.screen.OnboardingScreen @@ -27,12 +28,12 @@ fun AppContent( Scaffold { innerPadding -> NavHost( navController = navController, - startDestination = AppRouter.OnboardingRoute, //defaultRoute(state.activeChapterId), + startDestination = AppRouter.Onboarding.toString(), //defaultRoute(state.activeChapterId), modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { - composable(route = AppRouter.SplashRoute) { + composable(route = AppRouter.Splash.toString()) { SplashScreen { navController.navigateTo( from = AppRouter.Splash, @@ -41,7 +42,7 @@ fun AppContent( } } - composable(route = AppRouter.OnboardingRoute) { + composable(route = AppRouter.Onboarding.toString()) { OnboardingScreen( chapterList = viewModel.chapterList, onChapterSelected = { chapterId -> @@ -56,20 +57,42 @@ fun AppContent( ) } - composable(route = AppRouter.HomeRoute) { - MainScreen( + composable(route = AppRouter.Home.toString()) { + ScreenScaffold( viewModel = viewModel, + mainScreen = { viewModel, onEventDetailClicked -> + MainScreen( + viewModel = viewModel, + onEventDetailClicked = onEventDetailClicked + ) + }, + detailScreen = { viewModel, eventId -> + /** + * Need to show the detail screen on same composable screen + * due to side-to-side scaffold for tablet nor large screens. + */ + /** + * Need to show the detail screen on same composable screen + * due to side-to-side scaffold for tablet nor large screens. + */ + EventDetailScreen( + viewModel = viewModel, + eventId = eventId + ) + }, navigateToDetailScreen = { eventId -> - navController.navigate(AppRouter.constructEventDetailRoute(eventId)) + navController.navigate( + AppRouter.EventDetail.route.param(eventId) + ) } ) } composable( - route = AppRouter.EventDetailRoute, - arguments = listOf(navArgument(AppRouter.ArgumentEventId) { type = NavType.StringType }) + route = AppRouter.EventDetail.toString(), + arguments = listOf(navArgument(AppRouter.ARG_EVENT_ID) { type = NavType.StringType }) ) { backStackEntry -> - val eventId = backStackEntry.arguments?.getString(AppRouter.ArgumentEventId).orEmpty() + val eventId = backStackEntry.arguments?.getString(AppRouter.ARG_EVENT_ID).orEmpty() EventDetailScreen( viewModel = viewModel, @@ -81,7 +104,7 @@ fun AppContent( } private fun NavHostController.navigateTo(from: AppRouter? = null, to: AppRouter) { - navigate(to.route) { + navigate(to.route.toString()) { if (from != null) { // popUpTo(from.route) { // inclusive = true diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/AppRouter.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/AppRouter.kt index 162e009..a5c675e 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/AppRouter.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/AppRouter.kt @@ -1,26 +1,44 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + package id.gdg.app.ui -import id.gdg.app.ui.state.EventDetailUiModel +import kotlin.jvm.JvmInline -sealed class AppRouter(val route: String) { +sealed class AppRouter(val route: Path) { - data object Splash : AppRouter(SplashRoute) - data object Onboarding : AppRouter(OnboardingRoute) - data object Home : AppRouter(HomeRoute) + data object Splash : AppRouter(Path("splash")) + data object Onboarding : AppRouter(Path("onboarding")) + data object Home : AppRouter(Path("home")) + data object EventDetail : AppRouter(Path("detail/{${ARG_EVENT_ID}}")) - data class EventDetail(val eventId: Int) : AppRouter(EventDetailRoute) + override fun toString(): String { + return "gdg://$route" + } companion object { - val SplashRoute get() = "splash" - val OnboardingRoute get() = "onboarding" + const val ARG_EVENT_ID = "eventId" + } +} - val HomeRoute get() = "home" +@JvmInline +value class Path(private val value: String) { - // SAMPAH - val EventDetailRoute get() = "detail/{$ArgumentEventId}" - fun constructEventDetailRoute(eventId: String) = EventDetailRoute - .replace("{$ArgumentEventId}", eventId) + fun param(param: String): String { + return param(arrayOf(param)) + } - val ArgumentEventId get() = "eventId" + fun param(args: Array): String { + var result = value + val regex = Regex("\\{([^}]+)\\}") + val matches = regex.findAll(value) + if (matches.any()) { + matches.forEachIndexed { index, match -> + result = result.replace(match.value, args[index]) + } + } + + return result } + + override fun toString() = value } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt new file mode 100644 index 0000000..2f44e77 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt @@ -0,0 +1,66 @@ +package id.gdg.app.ui + +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.dp +import id.gdg.app.AppViewModel +import id.gdg.ui.TwoPanelScaffold +import id.gdg.ui.TwoPanelScaffoldAnimationSpec +import id.gdg.ui.androidx.compose.material3.windowsizeclass.CommonWindowSizeClass +import id.gdg.ui.androidx.compose.material3.windowsizeclass.CommonWindowWidthSizeClass +import id.gdg.ui.androidx.compose.material3.windowsizeclass.LocalWindowSizeClass + +@Composable +fun ScreenScaffold( + viewModel: AppViewModel, + mainScreen: @Composable (AppViewModel, onEventDetailClicked: (String) -> Unit) -> Unit, + detailScreen: @Composable (AppViewModel, String) -> Unit, + navigateToDetailScreen: (String) -> Unit +) { + var selectedEventId by rememberSaveable { mutableStateOf("") } + + val windowSizeClazz: CommonWindowSizeClass = LocalWindowSizeClass.current + var shouldPanelOpened: Boolean? by rememberSaveable { mutableStateOf(null) } + var panelVisibility by rememberSaveable { mutableStateOf(shouldPanelOpened != null) } + + LaunchedEffect(windowSizeClazz) { + shouldPanelOpened = shouldPanelOpened.takeIf { + windowSizeClazz.widthSizeClass != CommonWindowWidthSizeClass.Compact + } + + panelVisibility = shouldPanelOpened != null + } + + TwoPanelScaffold( + panelVisibility = panelVisibility, + animationSpec = TwoPanelScaffoldAnimationSpec( + finishedListener = { fraction -> if (fraction == 1f) shouldPanelOpened = null } + ), + body = { + mainScreen(viewModel) { + // If the screen size is compact (or mobile device screen size), then + // navigate to detail page with router. Otherwise, render the [panel]. + if (windowSizeClazz.widthSizeClass == CommonWindowWidthSizeClass.Compact) { + navigateToDetailScreen(it) + return@mainScreen + } + + selectedEventId = it + shouldPanelOpened = true + panelVisibility = true + } + }, + panel = { + Surface(tonalElevation = 1.dp) { + if (shouldPanelOpened != null) { + detailScreen(viewModel, selectedEventId) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt index 18d39c1..cc0b3f9 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt @@ -1,99 +1,33 @@ package id.gdg.app.ui.screen import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Surface 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.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.dp import id.gdg.app.AppViewModel import id.gdg.app.ui.AppEvent import id.gdg.app.ui.screen.content.PreviousEventContent import id.gdg.app.ui.screen.content.UpcomingEventContent -import id.gdg.app.ui.state.ChapterUiModel -import id.gdg.ui.TwoPanelScaffold -import id.gdg.ui.TwoPanelScaffoldAnimationSpec -import id.gdg.ui.androidx.compose.material3.windowsizeclass.CommonWindowSizeClass -import id.gdg.ui.androidx.compose.material3.windowsizeclass.CommonWindowWidthSizeClass -import id.gdg.ui.androidx.compose.material3.windowsizeclass.LocalWindowSizeClass @Composable fun MainScreen( viewModel: AppViewModel, - navigateToDetailScreen: (String) -> Unit + onEventDetailClicked: (String) -> Unit ) { val chapterUiState by viewModel.chapterUiState.collectAsState() - var selectedEventId by rememberSaveable { mutableStateOf("") } - - val windowSizeClazz: CommonWindowSizeClass = LocalWindowSizeClass.current - var shouldPanelOpened: Boolean? by rememberSaveable { mutableStateOf(null) } - var panelVisibility by rememberSaveable { mutableStateOf(shouldPanelOpened != null) } LaunchedEffect(Unit) { viewModel.sendEvent(AppEvent.InitialContent) } - LaunchedEffect(windowSizeClazz) { - shouldPanelOpened = - shouldPanelOpened.takeIf { windowSizeClazz.widthSizeClass != CommonWindowWidthSizeClass.Compact } - panelVisibility = shouldPanelOpened != null - } - - TwoPanelScaffold( - panelVisibility = panelVisibility, - animationSpec = TwoPanelScaffoldAnimationSpec( - finishedListener = { fraction -> if (fraction == 1f) shouldPanelOpened = null } - ), - body = { - MainScreenContent( - chapterUiState = chapterUiState, - onEventDetailClicked = { - // If the screen size is compact (or mobile device screen size), then - // navigate to detail page with router. Otherwise, render the [panel]. - if (windowSizeClazz.widthSizeClass == CommonWindowWidthSizeClass.Compact) { - navigateToDetailScreen(it) - return@MainScreenContent - } - - selectedEventId = it - shouldPanelOpened = true - panelVisibility = true - }, - onRefreshPreviousContentClicked = { - viewModel.sendEvent(AppEvent.FetchPreviousEvent) - } - ) - }, - panel = { - Surface(tonalElevation = 1.dp) { - if (shouldPanelOpened != null) { - EventDetailScreen( - viewModel = viewModel, - eventId = selectedEventId - ) - } - } - } - ) -} - -@Composable -fun MainScreenContent( - chapterUiState: ChapterUiModel, - onEventDetailClicked: (String) -> Unit, - onRefreshPreviousContentClicked: () -> Unit -) { Column { UpcomingEventContent(chapterUiState.upcomingEvent) PreviousEventContent( data = chapterUiState.previousEvents, onRefreshContent = { - onRefreshPreviousContentClicked() + viewModel.sendEvent(AppEvent.FetchPreviousEvent) }, onEventClicked = { onEventDetailClicked(it)