Neon/ui/src/commonMain/kotlin/moe/lava/neon/ui/App.kt
Cilly Leang 8e02b98c51
feat(ui): initial navigator prototype
Will be handling guild/channel browsing

- Bumped CMP material 3, so we don't have to use jetpack anymore
- Added coil for images later on
- Added a bajillion placeholder images
2026-01-30 01:16:32 +11:00

114 lines
4.4 KiB
Kotlin

package moe.lava.neon.ui
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MotionScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.savedstate.serialization.SavedStateConfiguration
import co.touchlab.kermit.Logger
import dev.zacsweers.metro.createGraph
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import moe.lava.neon.ui.di.AppUiGraph
import moe.lava.neon.ui.screens.Login
import moe.lava.neon.ui.screens.Sample
import moe.lava.neon.ui.screens.navigator.Navigator
import moe.lava.neon.ui.screens.navigator.NavigatorModel
import moe.lava.neon.ui.screens.navigator.NavigatorPreviewProvider
import kotlin.system.exitProcess
private object Route {
@Serializable
data object Login : NavKey
@Serializable
data object Sample : NavKey
@Serializable
data class Navigator(val left: Boolean) : NavKey
}
private val config = SavedStateConfiguration {
serializersModule = SerializersModule {
polymorphic(NavKey::class) {
subclass(Route.Login::class, Route.Login.serializer())
subclass(Route.Sample::class, Route.Sample.serializer())
subclass(Route.Navigator::class, Route.Navigator.serializer())
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun App() {
Thread.setDefaultUncaughtExceptionHandler { t: Thread, e: Throwable ->
Logger.withTag("neon.thrhandler").a("Uncaught from $t: ${e}\n${e.stackTraceToString()}", e)
exitProcess(1)
}
val uiGraph = createGraph<AppUiGraph>()
val graph = uiGraph.core
CaptchaBinder(graph.api)
CompositionLocalProvider(LocalMetroViewModelFactory provides uiGraph.metroViewModelFactory) {
MaterialExpressiveTheme(
colorScheme = getColorScheme(),
motionScheme = MotionScheme.expressive(),
) {
val init = if (graph.auth.token != null) Route.Sample else Route.Login
val backStack = rememberNavBackStack(config, init)
NavDisplay(
backStack = backStack,
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<Route.Login> {
Login(
onSuccess = {
backStack.clear()
backStack.add(Route.Sample)
}
)
}
entry<Route.Sample> {
Sample(
navTest = {
backStack.add(Route.Navigator(it))
},
onRequestLogout = {
backStack.clear()
backStack.add(Route.Login)
}
)
}
entry<Route.Navigator> { key ->
if (key.left) {
Navigator(
NavigatorPreviewProvider.base2.copy(
guildNavPosition = NavigatorModel.GuildNavPosition.LeftSidebar
)
)
} else {
Navigator(
NavigatorPreviewProvider.base2.copy(
guildNavPosition = NavigatorModel.GuildNavPosition.BottomSheet
)
)
}
}
}
)
}
}
}