diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 82a128c38..5bb612e47 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { implementation(libs.compose.ui.tooling.preview) implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.navigation3.ui) } androidUnitTest.dependencies { diff --git a/androidApp/src/androidMain/kotlin/org/jetbrains/kotlinconf/android/MainActivity.kt b/androidApp/src/androidMain/kotlin/org/jetbrains/kotlinconf/android/MainActivity.kt index df6736a22..d5b5671c8 100644 --- a/androidApp/src/androidMain/kotlin/org/jetbrains/kotlinconf/android/MainActivity.kt +++ b/androidApp/src/androidMain/kotlin/org/jetbrains/kotlinconf/android/MainActivity.kt @@ -8,11 +8,6 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.mmk.kmpnotifier.extensions.onCreateOrOnNewIntent import com.mmk.kmpnotifier.notification.NotifierManager @@ -48,14 +43,6 @@ class MainActivity : ComponentActivity() { window.isNavigationBarContrastEnforced = false } }, - popEnterTransition = { - scaleIn(initialScale = 1.05f) + - fadeIn(animationSpec = tween(50)) - }, - popExitTransition = { - scaleOut(targetScale = 0.9f, animationSpec = tween(50)) + - fadeOut(animationSpec = tween(50, delayMillis = 50)) - }, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d1bb5de5..0604d84fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ androidx-activityCompose = "1.11.0" androidx-core-ktx = "1.17.0" androidx-core-splashscreen = "1.0.1" androidx-lifecycle = "2.10.0-alpha04" -androidx-navigation = "2.9.1" +androidx-navigation3 = "1.0.0-alpha04" androidx-preference = "1.2.1" coil = "3.3.0" compose-multiplatform = "1.10.0-beta01" @@ -41,7 +41,8 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" } androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +androidx-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle" } +androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } @@ -50,7 +51,6 @@ compose-components-resources = { module = "org.jetbrains.compose.components:comp compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" } compose-material-ripple = { module = "org.jetbrains.compose.material:material-ripple", version.ref = "compose-multiplatform" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" } -compose-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" } compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" } compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" } doistx-normalize = { module = "com.doist.x:normalize", version.ref = "doistx-normalize" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 59407f141..28daa4f04 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -83,8 +83,8 @@ kotlin { implementation(libs.kotlinx.datetime) implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.navigation.compose) - implementation(libs.compose.ui.backhandler) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.ktor.client.core) implementation(libs.aboutlibraries.core) diff --git a/shared/src/androidMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.android.kt b/shared/src/androidMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.android.kt deleted file mode 100644 index e5e58c8db..000000000 --- a/shared/src/androidMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.android.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.jetbrains.kotlinconf.navigation - -import androidx.compose.runtime.Composable -import androidx.navigation.NavHostController - -@Composable -internal actual fun PlatformNavHandler(navController: NavHostController) { - // Nothing to do -} diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/App.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/App.kt index b3b9c4d8b..6a0bca2d7 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/App.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/App.kt @@ -1,8 +1,5 @@ package org.jetbrains.kotlinconf -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box @@ -13,17 +10,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry import org.jetbrains.kotlinconf.navigation.KotlinConfNavHost import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme import org.koin.compose.koinInject -import kotlin.jvm.JvmSuppressWildcards @Composable fun App( onThemeChange: ((isDarkTheme: Boolean) -> Unit)? = null, - popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition)? = null, - popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition)? = null, ) { val service = koinInject() val currentTheme by service.getTheme().collectAsStateWithLifecycle(initialValue = Theme.SYSTEM) @@ -53,7 +46,7 @@ fun App( .background(KotlinConfTheme.colors.mainBackground) ) { if (isOnboardingComplete != null) { - KotlinConfNavHost(isOnboardingComplete, popEnterTransition, popExitTransition) + KotlinConfNavHost(isOnboardingComplete) } } } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/AppRoute.kt similarity index 58% rename from shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt rename to shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/AppRoute.kt index 8ff003811..aa6314b49 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/Routes.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/AppRoute.kt @@ -6,109 +6,91 @@ import org.jetbrains.kotlinconf.PartnerId import org.jetbrains.kotlinconf.SessionId import org.jetbrains.kotlinconf.SpeakerId +@Serializable +sealed interface AppRoute + @Serializable @SerialName("AboutConference") -data object AboutConferenceScreen +data object AboutConferenceScreen : AppRoute @Serializable @SerialName("CodeOfConduct") -data object CodeOfConductScreen +data object CodeOfConductScreen : AppRoute @Serializable @SerialName("AboutApp") -data object AboutAppScreen - -@Serializable -@SerialName("Info") -data object InfoScreen - -@Serializable -@SerialName("Welcome") -data object StartScreens +data object AboutAppScreen : AppRoute @Serializable @SerialName("WelcomePrivacyNotice") -data object StartPrivacyNoticeScreen +data object StartPrivacyNoticeScreen : AppRoute @Serializable @SerialName("WelcomeSetupNotifications") -data object StartNotificationsScreen +data object StartNotificationsScreen : AppRoute @Serializable @SerialName("AppPrivacyNoticePrompt") -data object AppPrivacyNoticePrompt +data object AppPrivacyNoticePrompt : AppRoute @Serializable @SerialName("Settings") -data object SettingsScreen +data object SettingsScreen : AppRoute @Serializable @SerialName("VisitorPrivacyNotice") -data object VisitorPrivacyNoticeScreen +data object VisitorPrivacyNoticeScreen : AppRoute @Serializable @SerialName("AppPrivacyNotice") -data object AppPrivacyNoticeScreen +data object AppPrivacyNoticeScreen : AppRoute @Serializable @SerialName("TermsOfUse") -data object TermsOfUseScreen +data object TermsOfUseScreen : AppRoute @Serializable @SerialName("AppTermsOfUse") -data object AppTermsOfUseScreen +data object AppTermsOfUseScreen : AppRoute @Serializable @SerialName("Licenses") -data object LicensesScreen +data object LicensesScreen : AppRoute @Serializable @SerialName("License") data class SingleLicenseScreen( val licenseName: String, val licenseText: String, -) +) : AppRoute @Serializable @SerialName("Partners") -data object PartnersScreen +data object PartnersScreen : AppRoute @Serializable @SerialName("Partner") -data class PartnerDetailScreen(val partnerId: PartnerId) +data class PartnerDetailScreen(val partnerId: PartnerId) : AppRoute @Serializable @SerialName("Main") -data object MainScreen - -@Serializable -@SerialName("Schedule") -data object ScheduleScreen +data object MainScreen : AppRoute @Serializable @SerialName("Session") data class SessionScreen( val sessionId: SessionId, val openedForFeedback: Boolean = false, -) - -@Serializable -@SerialName("Speakers") -data object SpeakersScreen +) : AppRoute @Serializable @SerialName("Speaker") -data class SpeakerDetailScreen(val speakerId: SpeakerId) - -@Serializable -@SerialName("Map") -data object MapScreen +data class SpeakerDetailScreen(val speakerId: SpeakerId) : AppRoute @Serializable @SerialName("MapDetail") -data class NestedMapScreen(val roomName: String) - +data class NestedMapScreen(val roomName: String) : AppRoute @Serializable @SerialName("DeveloperMenu") -data object DeveloperMenuScreen +data object DeveloperMenuScreen : AppRoute diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt index 374762100..ea3bb03d7 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt @@ -1,27 +1,20 @@ package org.jetbrains.kotlinconf.navigation -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.ui.platform.LocalUriHandler -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navigation -import androidx.navigation.toRoute +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer import kotlinx.coroutines.channels.Channel import org.jetbrains.kotlinconf.LocalFlags import org.jetbrains.kotlinconf.LocalNotificationId -import org.jetbrains.kotlinconf.PartnerId import org.jetbrains.kotlinconf.SessionId -import org.jetbrains.kotlinconf.SpeakerId import org.jetbrains.kotlinconf.URLs import org.jetbrains.kotlinconf.screens.AboutAppScreen import org.jetbrains.kotlinconf.screens.AboutConference @@ -43,8 +36,6 @@ import org.jetbrains.kotlinconf.screens.StartNotificationsScreen import org.jetbrains.kotlinconf.screens.VisitorPrivacyNotice import org.jetbrains.kotlinconf.screens.VisitorTermsOfUse import org.jetbrains.kotlinconf.utils.getStoreUrl -import kotlin.jvm.JvmSuppressWildcards -import kotlin.reflect.typeOf fun navigateByLocalNotificationId(notificationId: String) { LocalNotificationId.parse(notificationId)?.let { @@ -59,220 +50,199 @@ fun navigateToSession(sessionId: SessionId) { notificationNavRequests.trySend(SessionScreen(sessionId)) } -private val notificationNavRequests = Channel(capacity = 1) +private val notificationNavRequests = Channel(capacity = 1) @Composable -private fun NotificationHandler(navController: NavHostController) { +private fun NotificationHandler(backStack: MutableList) { LaunchedEffect(Unit) { while (true) { - val destination = notificationNavRequests.receive() - navController.navigate(destination) + backStack.add(notificationNavRequests.receive()) } } } @Composable -internal fun KotlinConfNavHost( - isOnboardingComplete: Boolean, - popEnterTransition: @JvmSuppressWildcards (AnimatedContentTransitionScope.() -> EnterTransition)?, - popExitTransition: @JvmSuppressWildcards (AnimatedContentTransitionScope.() -> ExitTransition)?, -) { - val navController = rememberNavController() - - NotificationHandler(navController) - PlatformNavHandler(navController) - - val startDestination = if (isOnboardingComplete) MainScreen else StartScreens - if (popEnterTransition != null && popExitTransition != null) { - NavHost( - navController = navController, - startDestination = startDestination, - modifier = Modifier.fillMaxSize(), - popEnterTransition = popEnterTransition, - popExitTransition = popExitTransition, - ) { - screens(navController) - } - } else { - NavHost( - navController = navController, - startDestination = startDestination, - modifier = Modifier.fillMaxSize(), - ) { - screens(navController) +internal fun KotlinConfNavHost(isOnboardingComplete: Boolean) { + val backstack: MutableList = + rememberSerializable(serializer = SnapshotStateListSerializer()) { + val startDestination = if (isOnboardingComplete) MainScreen else StartPrivacyNoticeScreen + mutableStateListOf(startDestination) } - } -} + // TODO Integrate with browser navigation here https://github.com/JetBrains/kotlinconf-app/issues/557 -fun NavGraphBuilder.screens(navController: NavHostController) { - startScreens( - navController = navController, + NotificationHandler(backstack) + + NavDisplay( + backStack = backstack, + entryProvider = entryProvider { + screens(backstack) + }, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), ) +} - composable { - MainScreen( - rootNavController = navController, +private fun EntryProviderScope.screens(backStack: MutableList) { + entry { + val skipNotifications = LocalFlags.current.supportsNotifications.not() + AppPrivacyNoticePrompt( + onRejectNotice = { + if (skipNotifications) { + backStack.clear() + backStack.add(MainScreen) + } else { + backStack.add(StartNotificationsScreen) + } + }, + onAcceptNotice = { + if (skipNotifications) { + backStack.clear() + backStack.add(MainScreen) + } else { + backStack.add(StartNotificationsScreen) + } + }, + onAppTermsOfUse = { backStack.add(AppTermsOfUseScreen) }, + confirmationRequired = false, + ) + } + entry { + StartNotificationsScreen( + onDone = { + backStack.clear() + backStack.add(MainScreen) + } ) } - composable(typeMap = mapOf(typeOf() to SpeakerIdNavType)) { + entry { + MainScreen(onNavigate = { backStack.add(it) }) + } + entry { SpeakerDetailScreen( - speakerId = it.toRoute().speakerId, - onBack = navController::navigateUp, - onSession = { navController.navigate(SessionScreen(it)) }, + speakerId = it.speakerId, + onBack = backStack::removeLastOrNull, + onSession = { backStack.add(SessionScreen(it)) }, ) } - composable { + entry { + val urlHandler = LocalUriHandler.current + SessionScreen( + sessionId = it.sessionId, + openedForFeedback = it.openedForFeedback, + onBack = backStack::removeLastOrNull, + onPrivacyNoticeNeeded = { backStack.add(AppPrivacyNoticePrompt) }, + onSpeaker = { speakerId -> backStack.add(SpeakerDetailScreen(speakerId)) }, + onWatchVideo = { videoUrl -> urlHandler.openUri(videoUrl) }, + onNavigateToMap = { roomName -> + backStack.add(NestedMapScreen(roomName)) + }, + ) + } + + entry { val uriHandler = LocalUriHandler.current AboutAppScreen( - onBack = navController::navigateUp, + onBack = backStack::removeLastOrNull, onGitHubRepo = { uriHandler.openUri(URLs.GITHUB_REPO) }, onRateApp = { getStoreUrl()?.let { uriHandler.openUri(it) } }, - onPrivacyNotice = { navController.navigate(AppPrivacyNoticeScreen) }, - onTermsOfUse = { navController.navigate(AppTermsOfUseScreen) }, - onLicenses = { navController.navigate(LicensesScreen) }, + onPrivacyNotice = { backStack.add(AppPrivacyNoticeScreen) }, + onTermsOfUse = { backStack.add(AppTermsOfUseScreen) }, + onLicenses = { backStack.add(LicensesScreen) }, onJunie = { uriHandler.openUri(URLs.JUNIE_LANDING_PAGE) }, - onDeveloperMenu = { navController.navigate(DeveloperMenuScreen) }, + onDeveloperMenu = { backStack.add(DeveloperMenuScreen) }, ) } - composable { + entry { LicensesScreen( onLicenseClick = { licenseName, licenseText -> - navController.navigate(SingleLicenseScreen(licenseName, licenseText)) + backStack.add(SingleLicenseScreen(licenseName, licenseText)) }, - onBack = navController::navigateUp, + onBack = backStack::removeLastOrNull, ) } - composable { - val params = it.toRoute() + entry { SingleLicenseScreen( - licenseName = params.licenseName, - licenseContent = params.licenseText, - onBack = navController::navigateUp, + licenseName = it.licenseName, + licenseContent = it.licenseText, + onBack = backStack::removeLastOrNull, ) } - composable { + entry { val urlHandler = LocalUriHandler.current AboutConference( - onPrivacyNotice = { navController.navigate(VisitorPrivacyNoticeScreen) }, - onGeneralTerms = { navController.navigate(TermsOfUseScreen) }, + onPrivacyNotice = { backStack.add(VisitorPrivacyNoticeScreen) }, + onGeneralTerms = { backStack.add(TermsOfUseScreen) }, onWebsiteLink = { urlHandler.openUri(URLs.KOTLINCONF_HOMEPAGE) }, - onBack = navController::navigateUp, - onSpeaker = { speakerId -> navController.navigate(SpeakerDetailScreen(speakerId)) }, + onBack = backStack::removeLastOrNull, + onSpeaker = { speakerId -> backStack.add(SpeakerDetailScreen(speakerId)) }, ) } - composable { - CodeOfConduct(onBack = navController::navigateUp) + entry { + CodeOfConduct(onBack = backStack::removeLastOrNull) } - composable { - SettingsScreen(onBack = navController::navigateUp) + entry { + SettingsScreen(onBack = backStack::removeLastOrNull) } - composable { - VisitorPrivacyNotice(onBack = navController::navigateUp) + entry { + VisitorPrivacyNotice(onBack = backStack::removeLastOrNull) } - composable { + entry { AppPrivacyNotice( - onBack = navController::navigateUp, - onAppTermsOfUse = { navController.navigate(AppTermsOfUseScreen) }, + onBack = backStack::removeLastOrNull, + onAppTermsOfUse = { backStack.add(AppTermsOfUseScreen) }, ) } - composable { + entry { VisitorTermsOfUse( - onBack = navController::navigateUp, - onCodeOfConduct = { navController.navigate(CodeOfConductScreen) }, - onVisitorPrivacyNotice = { navController.navigate(VisitorPrivacyNoticeScreen) }, + onBack = backStack::removeLastOrNull, + onCodeOfConduct = { backStack.add(CodeOfConductScreen) }, + onVisitorPrivacyNotice = { backStack.add(VisitorPrivacyNoticeScreen) }, ) } - composable { + entry { AppTermsOfUse( - onBack = navController::navigateUp, + onBack = backStack::removeLastOrNull, onAppPrivacyNotice = { - navController.navigate(AppPrivacyNoticeScreen) + backStack.add(AppPrivacyNoticeScreen) }, ) } - composable { + entry { PartnersScreen( - onBack = navController::navigateUp, + onBack = backStack::removeLastOrNull, onPartnerDetail = { partnerId -> - navController.navigate(PartnerDetailScreen(partnerId)) + backStack.add(PartnerDetailScreen(partnerId)) } ) } - composable(typeMap = mapOf(typeOf() to PartnerIdNavType)) { + entry { PartnerDetailScreen( - partnerId = it.toRoute().partnerId, - onBack = navController::navigateUp, + partnerId = it.partnerId, + onBack = backStack::removeLastOrNull, ) } - composable(typeMap = mapOf(typeOf() to SessionIdNavType)) { - val params = it.toRoute() - val urlHandler = LocalUriHandler.current - SessionScreen( - sessionId = params.sessionId, - openedForFeedback = params.openedForFeedback, - onBack = navController::navigateUp, - onPrivacyNoticeNeeded = { navController.navigate(AppPrivacyNoticePrompt) }, - onSpeaker = { speakerId -> navController.navigate(SpeakerDetailScreen(speakerId)) }, - onWatchVideo = { videoUrl -> urlHandler.openUri(videoUrl) }, - onNavigateToMap = { roomName -> - navController.navigate(NestedMapScreen(roomName)) - }, - ) - } - composable { + + entry { AppPrivacyNoticePrompt( - onRejectNotice = navController::navigateUp, - onAcceptNotice = navController::navigateUp, - onAppTermsOfUse = { navController.navigate(AppTermsOfUseScreen) }, + onRejectNotice = backStack::removeLastOrNull, + onAcceptNotice = backStack::removeLastOrNull, + onAppTermsOfUse = { backStack.add(AppTermsOfUseScreen) }, confirmationRequired = true, ) } - composable { - DeveloperMenuScreen(onBack = navController::navigateUp) + entry { + DeveloperMenuScreen(onBack = backStack::removeLastOrNull) } - composable { - val nestedMapParam = it.toRoute() + entry { NestedMapScreen( - roomName = nestedMapParam.roomName, - onBack = navController::navigateUp, + roomName = it.roomName, + onBack = backStack::removeLastOrNull, ) } } - -fun NavGraphBuilder.startScreens( - navController: NavHostController, -) { - navigation( - startDestination = StartPrivacyNoticeScreen - ) { - composable { - val skipNotifications = LocalFlags.current.supportsNotifications.not() - AppPrivacyNoticePrompt( - onRejectNotice = { - navController.navigate(if (skipNotifications) MainScreen else StartNotificationsScreen) { - popUpTo { inclusive = skipNotifications } - } - }, - onAcceptNotice = { - navController.navigate(if (skipNotifications) MainScreen else StartNotificationsScreen) { - popUpTo { inclusive = skipNotifications } - } - }, - onAppTermsOfUse = { navController.navigate(AppTermsOfUseScreen) }, - confirmationRequired = false, - ) - } - composable { - StartNotificationsScreen( - onDone = { - navController.navigate(MainScreen) { - popUpTo { inclusive = true } - } - }) - } - } -} diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/MainRoute.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/MainRoute.kt new file mode 100644 index 000000000..0d1938fb0 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/MainRoute.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlinconf.navigation + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed interface MainRoute + +@Serializable +data object ScheduleScreen : MainRoute + +@Serializable +@SerialName("Speakers") +data object SpeakersScreen : MainRoute + +@Serializable +@SerialName("Map") +data object MapScreen : MainRoute + +@Serializable +@SerialName("Info") +data object InfoScreen : MainRoute diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.kt deleted file mode 100644 index 156df0224..000000000 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.jetbrains.kotlinconf.navigation - -import androidx.compose.runtime.Composable -import androidx.navigation.NavHostController - -/** - * Used for web target to synchronize the URL and browser history - */ -@Composable -internal expect fun PlatformNavHandler(navController: NavHostController) diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/LicenseScreens.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/LicenseScreens.kt index 8c5c46fae..fee6b4848 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/LicenseScreens.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/LicenseScreens.kt @@ -22,9 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.backhandler.BackHandler import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.liveRegion @@ -34,6 +32,9 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.compose.NavigationBackHandler +import androidx.navigationevent.compose.rememberNavigationEventState import com.mikepenz.aboutlibraries.entity.Library import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource @@ -124,11 +125,15 @@ fun LicensesScreen( ) }, searchContent = { - @OptIn(ExperimentalComposeUiApi::class) - BackHandler(true) { - searchState = MainHeaderContainerState.Title - searchText = "" - } + NavigationBackHandler( + state = rememberNavigationEventState(NavigationEventInfo.None), + isBackEnabled = true, + onBackCompleted = { + searchState = MainHeaderContainerState.Title + searchText = "" + }, + ) + MainHeaderSearchBar( searchValue = searchText, onSearchValueChange = { searchText = it }, diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt index a213272b7..f25a634bb 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/MainScreen.kt @@ -1,6 +1,8 @@ package org.jetbrains.kotlinconf.screens +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ContentTransform import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.snap @@ -16,22 +18,20 @@ import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.saveable.SaveableStateHolder +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.backhandler.BackHandler import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import org.jetbrains.compose.resources.stringResource +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.compose.NavigationBackHandler +import androidx.navigationevent.compose.rememberNavigationEventState import org.jetbrains.kotlinconf.ConferenceService import org.jetbrains.kotlinconf.LocalFlags import org.jetbrains.kotlinconf.URLs @@ -51,8 +51,10 @@ import org.jetbrains.kotlinconf.generated.resources.team_28_fill import org.jetbrains.kotlinconf.navigation.AboutAppScreen import org.jetbrains.kotlinconf.navigation.AboutConferenceScreen import org.jetbrains.kotlinconf.navigation.AppPrivacyNoticePrompt +import org.jetbrains.kotlinconf.navigation.AppRoute import org.jetbrains.kotlinconf.navigation.CodeOfConductScreen import org.jetbrains.kotlinconf.navigation.InfoScreen +import org.jetbrains.kotlinconf.navigation.MainRoute import org.jetbrains.kotlinconf.navigation.MapScreen import org.jetbrains.kotlinconf.navigation.PartnersScreen import org.jetbrains.kotlinconf.navigation.ScheduleScreen @@ -66,9 +68,11 @@ import org.jetbrains.kotlinconf.ui.components.MainNavigation import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme import org.koin.compose.koinInject +private val NoContentTransition = ContentTransform(EnterTransition.None, ExitTransition.None) + @Composable fun MainScreen( - rootNavController: NavController, + onNavigate: (AppRoute) -> Unit, service: ConferenceService = koinInject(), ) { LaunchedEffect(Unit) { @@ -81,66 +85,75 @@ fun MainScreen( .background(color = KotlinConfTheme.colors.mainBackground) .windowInsetsPadding(WindowInsets.safeDrawing) ) { - val nestedNavController = rememberNavController() - NavHost( - nestedNavController, - startDestination = ScheduleScreen, + var currentIndex by rememberSaveable { mutableIntStateOf(0) } + + val saveableStateHolder: SaveableStateHolder = rememberSaveableStateHolder() + + if (currentIndex > 0 && LocalFlags.current.enableBackOnMainScreens) { + NavigationBackHandler( + state = rememberNavigationEventState(NavigationEventInfo.None), + isBackEnabled = true, + onBackCompleted = { currentIndex = 0 }, + ) + } + + AnimatedContent( + targetState = currentIndex, modifier = Modifier .fillMaxWidth() .weight(1f), - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, - popEnterTransition = { EnterTransition.None }, - popExitTransition = { ExitTransition.None }, - ) { - composable { - MainBackHandler() - val uriHandler = LocalUriHandler.current - InfoScreen( - onAboutConf = { rootNavController.navigate(AboutConferenceScreen) }, - onAboutApp = { rootNavController.navigate(AboutAppScreen) }, - onOurPartners = { rootNavController.navigate(PartnersScreen) }, - onCodeOfConduct = { rootNavController.navigate(CodeOfConductScreen) }, - onTwitter = { uriHandler.openUri(URLs.TWITTER) }, - onSlack = { uriHandler.openUri(URLs.SLACK) }, - onBluesky = { uriHandler.openUri(URLs.BLUESKY) }, - onSettings = { rootNavController.navigate(SettingsScreen) }, - ) - } - composable { - MainBackHandler() - SpeakersScreen( - onSpeaker = { rootNavController.navigate(SpeakerDetailScreen(it)) } - ) - } - composable { - MainBackHandler() - ScheduleScreen( - onSession = { rootNavController.navigate(SessionScreen(it)) }, - onPrivacyNoticeNeeded = { rootNavController.navigate(AppPrivacyNoticePrompt) }, - onRequestFeedbackWithComment = { sessionId -> - rootNavController.navigate(SessionScreen(sessionId, openedForFeedback = true)) - }, - ) - } - composable { - MainBackHandler() - MapScreen() + transitionSpec = { NoContentTransition }, + ) { index -> + saveableStateHolder.SaveableStateProvider(index) { + MainScreenContent(bottomNavDestinations[index].route, onNavigate) } } AnimatedVisibility(!isKeyboardOpen(), enter = fadeIn(snap()), exit = fadeOut(snap())) { - BottomNavigation(nestedNavController) + BottomNavigation( + currentIndex = currentIndex, + onSelect = { selected -> currentIndex = selected } + ) } } } @Composable -private fun MainBackHandler() { - if (!LocalFlags.current.enableBackOnMainScreens) { - // Prevent back navigation with an empty handler - @OptIn(ExperimentalComposeUiApi::class) - BackHandler(true) { } +private fun MainScreenContent(route: MainRoute, onNavigate: (AppRoute) -> Unit) { + when (route) { + ScheduleScreen -> { + ScheduleScreen( + onSession = { onNavigate(SessionScreen(it)) }, + onPrivacyNoticeNeeded = { onNavigate(AppPrivacyNoticePrompt) }, + onRequestFeedbackWithComment = { sessionId -> + onNavigate(SessionScreen(sessionId, openedForFeedback = true)) + }, + ) + } + + SpeakersScreen -> { + SpeakersScreen( + onSpeaker = { onNavigate(SpeakerDetailScreen(it)) } + ) + } + + MapScreen -> { + MapScreen() + } + + InfoScreen -> { + val uriHandler = LocalUriHandler.current + InfoScreen( + onAboutConf = { onNavigate(AboutConferenceScreen) }, + onAboutApp = { onNavigate(AboutAppScreen) }, + onOurPartners = { onNavigate(PartnersScreen) }, + onCodeOfConduct = { onNavigate(CodeOfConductScreen) }, + onTwitter = { uriHandler.openUri(URLs.TWITTER) }, + onSlack = { uriHandler.openUri(URLs.SLACK) }, + onBluesky = { uriHandler.openUri(URLs.BLUESKY) }, + onSettings = { onNavigate(SettingsScreen) }, + ) + } } } @@ -150,61 +163,44 @@ private fun isKeyboardOpen(): Boolean { return rememberUpdatedState(bottomInset > 300).value } -@Composable -private fun BottomNavigation(nestedNavController: NavHostController) { - val bottomNavDestinations: List = - listOf( - MainNavDestination( - label = stringResource(Res.string.nav_destination_schedule), - icon = Res.drawable.clock_28, - iconSelected = Res.drawable.clock_28_fill, - route = ScheduleScreen, - routeClass = ScheduleScreen::class - ), - MainNavDestination( - label = stringResource(Res.string.nav_destination_speakers), - icon = Res.drawable.team_28, - iconSelected = Res.drawable.team_28_fill, - route = SpeakersScreen, - routeClass = SpeakersScreen::class - ), - MainNavDestination( - label = stringResource(Res.string.nav_destination_map), - icon = Res.drawable.location_28, - iconSelected = Res.drawable.location_28_fill, - route = MapScreen, - routeClass = MapScreen::class - ), - MainNavDestination( - label = stringResource(Res.string.nav_destination_info), - icon = Res.drawable.info_28, - iconSelected = Res.drawable.info_28_fill, - route = InfoScreen, - routeClass = InfoScreen::class - ), - ) - - val currentDestination = nestedNavController.currentBackStackEntryAsState().value?.destination - val currentBottomNavDestination = currentDestination?.let { - bottomNavDestinations.find { dest -> - val routeClass = dest.routeClass - routeClass != null && currentDestination.hasRoute(routeClass) - } - } +private val bottomNavDestinations: List> = listOf( + MainNavDestination( + label = Res.string.nav_destination_schedule, + icon = Res.drawable.clock_28, + iconSelected = Res.drawable.clock_28_fill, + route = ScheduleScreen, + ), + MainNavDestination( + label = Res.string.nav_destination_speakers, + icon = Res.drawable.team_28, + iconSelected = Res.drawable.team_28_fill, + route = SpeakersScreen, + ), + MainNavDestination( + label = Res.string.nav_destination_map, + icon = Res.drawable.location_28, + iconSelected = Res.drawable.location_28_fill, + route = MapScreen, + ), + MainNavDestination( + label = Res.string.nav_destination_info, + icon = Res.drawable.info_28, + iconSelected = Res.drawable.info_28_fill, + route = InfoScreen, + ), +) +@Composable +private fun BottomNavigation( + currentIndex: Int, + onSelect: (Int) -> Unit, +) { Divider(thickness = 1.dp, color = KotlinConfTheme.colors.strokePale) MainNavigation( - currentDestination = currentBottomNavDestination, + currentDestination = bottomNavDestinations[currentIndex], destinations = bottomNavDestinations, - onSelect = { - nestedNavController.navigate(it.route) { - // Avoid stacking multiple copies of the main screens - popUpTo(nestedNavController.graph.findStartDestination().route!!) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + onSelect = { selectedDestination -> + onSelect(bottomNavDestinations.indexOf(selectedDestination)) }, ) } diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/ScheduleScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/ScheduleScreen.kt index 350cfc9ac..7a821838c 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/ScheduleScreen.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/ScheduleScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.backhandler.BackHandler import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.semantics.CollectionInfo import androidx.compose.ui.semantics.CollectionItemInfo @@ -45,6 +44,9 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.compose.NavigationBackHandler +import androidx.navigationevent.compose.rememberNavigationEventState import kotlinx.coroutines.launch import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource @@ -367,11 +369,15 @@ private fun Header( ) }, searchContent = { - @OptIn(ExperimentalComposeUiApi::class) - BackHandler(true) { - onHeaderStateChange(MainHeaderContainerState.Title) - onSearchQueryChange("") - } + NavigationBackHandler( + state = rememberNavigationEventState(NavigationEventInfo.None), + isBackEnabled = true, + onBackCompleted = { + onHeaderStateChange(MainHeaderContainerState.Title) + onSearchQueryChange("") + }, + ) + val filterItems by viewModel.filterItems.collectAsStateWithLifecycle() MainHeaderSearchBar( diff --git a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/SpeakersScreen.kt b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/SpeakersScreen.kt index 65527c642..bee89d549 100644 --- a/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/SpeakersScreen.kt +++ b/shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/screens/SpeakersScreen.kt @@ -17,15 +17,16 @@ 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.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.backhandler.BackHandler import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.compose.NavigationBackHandler +import androidx.navigationevent.compose.rememberNavigationEventState import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.kotlinconf.HideKeyboardOnDragHandler @@ -90,11 +91,15 @@ fun SpeakersScreen( ) }, searchContent = { - @OptIn(ExperimentalComposeUiApi::class) - BackHandler(true) { - searchState = MainHeaderContainerState.Title - searchText = "" - } + NavigationBackHandler( + state = rememberNavigationEventState(NavigationEventInfo.None), + isBackEnabled = true, + onBackCompleted = { + searchState = MainHeaderContainerState.Title + searchText = "" + }, + ) + MainHeaderSearchBar( searchValue = searchText, onSearchValueChange = { searchText = it }, diff --git a/shared/src/iosMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.ios.kt b/shared/src/iosMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.ios.kt deleted file mode 100644 index e5e58c8db..000000000 --- a/shared/src/iosMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.ios.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.jetbrains.kotlinconf.navigation - -import androidx.compose.runtime.Composable -import androidx.navigation.NavHostController - -@Composable -internal actual fun PlatformNavHandler(navController: NavHostController) { - // Nothing to do -} diff --git a/shared/src/jsMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.js.kt b/shared/src/jsMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.js.kt deleted file mode 100644 index ee2ec49d4..000000000 --- a/shared/src/jsMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.js.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.jetbrains.kotlinconf.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.navigation.ExperimentalBrowserHistoryApi -import androidx.navigation.NavHostController -import androidx.navigation.bindToBrowserNavigation - -@OptIn(ExperimentalBrowserHistoryApi::class) -@Composable -internal actual fun PlatformNavHandler(navController: NavHostController) { - LaunchedEffect(Unit) { - navController.bindToBrowserNavigation() - } -} diff --git a/shared/src/jvmMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.jvm.kt b/shared/src/jvmMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.jvm.kt deleted file mode 100644 index e5e58c8db..000000000 --- a/shared/src/jvmMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.jvm.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.jetbrains.kotlinconf.navigation - -import androidx.compose.runtime.Composable -import androidx.navigation.NavHostController - -@Composable -internal actual fun PlatformNavHandler(navController: NavHostController) { - // Nothing to do -} diff --git a/shared/src/wasmJsMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.wasm.kt b/shared/src/wasmJsMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.wasm.kt deleted file mode 100644 index ee2ec49d4..000000000 --- a/shared/src/wasmJsMain/kotlin/org/jetbrains/kotlinconf/navigation/PlatformNavHandler.wasm.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.jetbrains.kotlinconf.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.navigation.ExperimentalBrowserHistoryApi -import androidx.navigation.NavHostController -import androidx.navigation.bindToBrowserNavigation - -@OptIn(ExperimentalBrowserHistoryApi::class) -@Composable -internal actual fun PlatformNavHandler(navController: NavHostController) { - LaunchedEffect(Unit) { - navController.bindToBrowserNavigation() - } -} diff --git a/ui-components/build.gradle.kts b/ui-components/build.gradle.kts index d37159a95..23175601b 100644 --- a/ui-components/build.gradle.kts +++ b/ui-components/build.gradle.kts @@ -42,7 +42,6 @@ kotlin { implementation(libs.coil.network.ktor3) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.navigation.compose) implementation(libs.multiplatform.markdown.renderer) } diff --git a/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/MainNavigation.kt b/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/MainNavigation.kt index e8a1ace81..872da52ca 100644 --- a/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/MainNavigation.kt +++ b/ui-components/src/commonMain/kotlin/org/jetbrains/kotlinconf/ui/components/MainNavigation.kt @@ -19,7 +19,9 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource import org.jetbrains.kotlinconf.ui.generated.resources.UiRes import org.jetbrains.kotlinconf.ui.generated.resources.clock_28 import org.jetbrains.kotlinconf.ui.generated.resources.clock_28_fill @@ -31,7 +33,6 @@ import org.jetbrains.kotlinconf.ui.generated.resources.team_28 import org.jetbrains.kotlinconf.ui.generated.resources.team_28_fill import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme import org.jetbrains.kotlinconf.ui.theme.PreviewHelper -import kotlin.reflect.KClass private val MainNavigationButtonShape = RoundedCornerShape(8.dp) @@ -39,7 +40,7 @@ private val MainNavigationButtonShape = RoundedCornerShape(8.dp) private fun MainNavigationButton( iconResource: DrawableResource, iconFilledResource: DrawableResource, - contentDescription: String, + contentDescription: String?, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -65,19 +66,18 @@ private fun MainNavigationButton( ) } -data class MainNavDestination( - val label: String, +data class MainNavDestination( + val label: StringResource?, val icon: DrawableResource, - val route: Any, + val route: T, val iconSelected: DrawableResource = icon, - val routeClass: KClass<*>? = null, ) @Composable -fun MainNavigation( - currentDestination: MainNavDestination?, - destinations: List, - onSelect: (MainNavDestination) -> Unit, +fun MainNavigation( + currentDestination: MainNavDestination?, + destinations: List>, + onSelect: (MainNavDestination) -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -88,7 +88,7 @@ fun MainNavigation( MainNavigationButton( iconResource = destination.icon, iconFilledResource = destination.iconSelected, - contentDescription = destination.label, + contentDescription = destination.label?.let { stringResource(it) }, selected = destination == currentDestination, onClick = { onSelect(destination) }, modifier = Modifier.weight(1f), @@ -102,36 +102,38 @@ fun MainNavigation( internal fun MainNavigationPreview() { PreviewHelper { var currentDestination by remember { - mutableStateOf(MainNavDestination( - label = "Schedule", - icon = UiRes.drawable.clock_28, - iconSelected = UiRes.drawable.clock_28_fill, - route = "Schedule" - )) + mutableStateOf( + MainNavDestination( + label = null, + icon = UiRes.drawable.clock_28, + iconSelected = UiRes.drawable.clock_28_fill, + route = "Schedule" + ) + ) } MainNavigation( currentDestination = currentDestination, destinations = listOf( MainNavDestination( - label = "Info", + label = null, icon = UiRes.drawable.info_28, iconSelected = UiRes.drawable.info_28_fill, route = "Info" ), MainNavDestination( - label = "Schedule", + label = null, icon = UiRes.drawable.clock_28, iconSelected = UiRes.drawable.clock_28_fill, route = "Schedule" ), MainNavDestination( - label = "Speakers", + label = null, icon = UiRes.drawable.team_28, iconSelected = UiRes.drawable.team_28_fill, route = "Speakers" ), MainNavDestination( - label = "Map", + label = null, icon = UiRes.drawable.location_28, iconSelected = UiRes.drawable.location_28_fill, route = "Map"