refactor: convert navigation to type-safe routes with polymorphic serialization
This commit is contained in:
parent
9be9a57b9b
commit
2756efa812
16 changed files with 412 additions and 278 deletions
|
|
@ -2,6 +2,7 @@ plugins {
|
|||
id("librechat.mobile.application")
|
||||
id("librechat.mobile.compose")
|
||||
id("librechat.mobile.koin")
|
||||
id("librechat.kotlin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.garfiec.librechat.navigation
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appModule = module {
|
||||
viewModelOf(::NavHostViewModel)
|
||||
single<SerializersModule>(named("navigation")) { navigationSerializersModule }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,42 +32,58 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.toRoute
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlin.reflect.KClass
|
||||
import com.garfiec.librechat.MainActivity
|
||||
import com.garfiec.librechat.R
|
||||
import com.garfiec.librechat.core.ui.components.BannerDisplay
|
||||
import com.garfiec.librechat.feature.agents.navigation.AGENT_EDITOR_CREATE_ROUTE
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentDetail
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentEditorCreate
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentEditorEdit
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentMarketplace
|
||||
import com.garfiec.librechat.feature.agents.navigation.agentsGraph
|
||||
import com.garfiec.librechat.feature.auth.navigation.AUTH_GRAPH_ROUTE
|
||||
import com.garfiec.librechat.feature.auth.navigation.AuthRoute
|
||||
import com.garfiec.librechat.feature.auth.navigation.ServerUrl
|
||||
import com.garfiec.librechat.feature.auth.navigation.authGraph
|
||||
import com.garfiec.librechat.feature.chat.navigation.CHAT_GRAPH_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.NEW_CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.Chat
|
||||
import com.garfiec.librechat.feature.chat.navigation.ChatRoute
|
||||
import com.garfiec.librechat.feature.chat.navigation.NewChat
|
||||
import com.garfiec.librechat.feature.chat.navigation.chatGraph
|
||||
import com.garfiec.librechat.feature.chat.navigation.navigateToChat
|
||||
import com.garfiec.librechat.feature.conversations.navigation.ArchivedConversations
|
||||
import com.garfiec.librechat.feature.conversations.navigation.conversationsGraph
|
||||
import com.garfiec.librechat.feature.files.navigation.Files
|
||||
import com.garfiec.librechat.feature.files.navigation.filesGraph
|
||||
import com.garfiec.librechat.feature.settings.navigation.API_KEYS_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.PRESET_MANAGER_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_ACCOUNT_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_DATA_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_GENERAL_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_TABBED_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SHARED_LINKS_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.ApiKeys
|
||||
import com.garfiec.librechat.feature.settings.navigation.PresetManager
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsAccount
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsChat
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsData
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsGeneral
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsRoute
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsTabbed
|
||||
import com.garfiec.librechat.feature.settings.navigation.SharedLinks
|
||||
import com.garfiec.librechat.feature.settings.navigation.settingsGraph
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
/** Maps a [SettingsCategory] to its corresponding navigation route. */
|
||||
fun SettingsCategory.toRoute(): String = when (this) {
|
||||
SettingsCategory.GENERAL -> SETTINGS_GENERAL_ROUTE
|
||||
SettingsCategory.CHAT -> SETTINGS_CHAT_ROUTE
|
||||
SettingsCategory.ACCOUNT -> SETTINGS_ACCOUNT_ROUTE
|
||||
SettingsCategory.DATA -> SETTINGS_DATA_ROUTE
|
||||
/** Checks if this destination's route matches the given typed route class. */
|
||||
private fun NavDestination?.isRoute(routeClass: KClass<*>): Boolean {
|
||||
val qualifiedName = routeClass.qualifiedName ?: return false
|
||||
return this?.route?.startsWith(qualifiedName) == true
|
||||
}
|
||||
|
||||
/** Maps a [SettingsCategory] to its corresponding typed navigation route. */
|
||||
fun SettingsCategory.toRoute(): SettingsRoute = when (this) {
|
||||
SettingsCategory.GENERAL -> SettingsGeneral
|
||||
SettingsCategory.CHAT -> SettingsChat
|
||||
SettingsCategory.ACCOUNT -> SettingsAccount
|
||||
SettingsCategory.DATA -> SettingsData
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -85,14 +101,13 @@ fun LibreChatNavHost(
|
|||
|
||||
val isLoggedIn by navHostViewModel.isLoggedIn.collectAsStateWithLifecycle()
|
||||
|
||||
val isInAuthFlow = currentDestination?.route == AUTH_GRAPH_ROUTE ||
|
||||
currentDestination?.parent?.route == AUTH_GRAPH_ROUTE
|
||||
val isInAuthFlow = currentDestination.isRoute(AuthRoute::class) ||
|
||||
currentDestination?.parent.isRoute(AuthRoute::class)
|
||||
|
||||
// Track active conversation from nav back stack
|
||||
LaunchedEffect(navBackStackEntry) {
|
||||
val route = navBackStackEntry?.destination?.route
|
||||
val conversationId = if (route == CHAT_ROUTE) {
|
||||
navBackStackEntry?.arguments?.getString("conversationId")
|
||||
val conversationId = if (navBackStackEntry?.destination.isRoute(Chat::class)) {
|
||||
navBackStackEntry?.toRoute<Chat>()?.conversationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -102,13 +117,13 @@ fun LibreChatNavHost(
|
|||
// Handle session expiry
|
||||
LaunchedEffect(Unit) {
|
||||
navHostViewModel.sessionExpired.collect {
|
||||
navController.navigate(AUTH_GRAPH_ROUTE) {
|
||||
navController.navigate(ServerUrl) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val startDestination = if (isLoggedIn) CHAT_GRAPH_ROUTE else AUTH_GRAPH_ROUTE
|
||||
val startDestination: Any = if (isLoggedIn) ChatRoute::class else AuthRoute::class
|
||||
|
||||
val isTablet = windowSizeClass?.widthSizeClass?.let {
|
||||
it >= WindowWidthSizeClass.Medium
|
||||
|
|
@ -193,7 +208,7 @@ private fun VersionMismatchDialog(
|
|||
private fun PhoneLayout(
|
||||
navController: androidx.navigation.NavHostController,
|
||||
navHostViewModel: NavHostViewModel,
|
||||
startDestination: String,
|
||||
startDestination: Any,
|
||||
isInAuthFlow: Boolean,
|
||||
deepLinkUri: Uri?,
|
||||
onDeepLinkConsumed: () -> Unit,
|
||||
|
|
@ -227,11 +242,12 @@ private fun PhoneLayout(
|
|||
// non-chat screen (e.g. settings, agents, files).
|
||||
LaunchedEffect(shareNavigationTrigger) {
|
||||
if (shareNavigationTrigger > 0) {
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
val isOnChatScreen = currentRoute == CHAT_ROUTE || currentRoute == NEW_CHAT_ROUTE
|
||||
val currentDest = navController.currentBackStackEntry?.destination
|
||||
val isOnChatScreen = currentDest.isRoute(Chat::class) ||
|
||||
currentDest.isRoute(NewChat::class)
|
||||
if (!isOnChatScreen) {
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
|
@ -257,10 +273,10 @@ private fun PhoneLayout(
|
|||
onNewChat = {
|
||||
scope.launch { drawerState.close() }
|
||||
// Skip navigation if already on the new chat screen
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
if (currentRoute != NEW_CHAT_ROUTE) {
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
val currentDest = navController.currentBackStackEntry?.destination
|
||||
if (!currentDest.isRoute(NewChat::class)) {
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +287,7 @@ private fun PhoneLayout(
|
|||
},
|
||||
onSettingsClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
navController.navigate(SETTINGS_TABBED_ROUTE) {
|
||||
navController.navigate(SettingsTabbed) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -283,13 +299,13 @@ private fun PhoneLayout(
|
|||
},
|
||||
onAgentsClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
navController.navigate(TopLevelDestination.AGENTS.route) {
|
||||
navController.navigate(AgentMarketplace) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onFilesClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
navController.navigate(TopLevelDestination.FILES.route) {
|
||||
navController.navigate(Files) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -343,8 +359,8 @@ private fun PhoneLayout(
|
|||
navController = navController,
|
||||
onAuthComplete = {
|
||||
navHostViewModel.onAuthComplete()
|
||||
navController.navigate(CHAT_GRAPH_ROUTE) {
|
||||
popUpTo(AUTH_GRAPH_ROUTE) { inclusive = true }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<AuthRoute> { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -359,7 +375,7 @@ private fun PhoneLayout(
|
|||
navController.navigateToChat(conversationId)
|
||||
},
|
||||
onNavigateToArchived = {
|
||||
navController.navigate("conversations/archived")
|
||||
navController.navigate(ArchivedConversations)
|
||||
},
|
||||
onNavigateBackFromArchived = {
|
||||
navController.popBackStack()
|
||||
|
|
@ -367,38 +383,38 @@ private fun PhoneLayout(
|
|||
)
|
||||
agentsGraph(
|
||||
onAgentClick = { agentId ->
|
||||
navController.navigate("agents/$agentId")
|
||||
navController.navigate(AgentDetail(agentId = agentId))
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
onStartChat = { agentId ->
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onCreateAgent = {
|
||||
navController.navigate(AGENT_EDITOR_CREATE_ROUTE)
|
||||
navController.navigate(AgentEditorCreate)
|
||||
},
|
||||
onEditAgent = { agentId ->
|
||||
navController.navigate("agents/editor/$agentId")
|
||||
navController.navigate(AgentEditorEdit(agentId = agentId))
|
||||
},
|
||||
)
|
||||
filesGraph()
|
||||
settingsGraph(
|
||||
onLogout = {
|
||||
navHostViewModel.logout()
|
||||
navController.navigate(AUTH_GRAPH_ROUTE) {
|
||||
navController.navigate(ServerUrl) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToArchived = {
|
||||
navController.navigate("conversations/archived") {
|
||||
navController.navigate(ArchivedConversations) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateToSharedLinks = {
|
||||
navController.navigate(SHARED_LINKS_ROUTE) {
|
||||
navController.navigate(SharedLinks) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -406,7 +422,7 @@ private fun PhoneLayout(
|
|||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToPresets = {
|
||||
navController.navigate(PRESET_MANAGER_ROUTE) {
|
||||
navController.navigate(PresetManager) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -414,7 +430,7 @@ private fun PhoneLayout(
|
|||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToApiKeys = {
|
||||
navController.navigate(API_KEYS_ROUTE) {
|
||||
navController.navigate(ApiKeys) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.garfiec.librechat.navigation
|
||||
|
||||
import com.garfiec.librechat.feature.agents.navigation.agentsSerializersModule
|
||||
import com.garfiec.librechat.feature.auth.navigation.authSerializersModule
|
||||
import com.garfiec.librechat.feature.chat.navigation.chatSerializersModule
|
||||
import com.garfiec.librechat.feature.conversations.navigation.conversationsSerializersModule
|
||||
import com.garfiec.librechat.feature.files.navigation.filesSerializersModule
|
||||
import com.garfiec.librechat.feature.settings.navigation.settingsSerializersModule
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.plus
|
||||
|
||||
/**
|
||||
* Combined [SerializersModule] for all navigation route types.
|
||||
* Registers polymorphic serializers for each feature module's sealed route hierarchy.
|
||||
* This will be used by Nav 3's SavedStateConfiguration for type-safe state saving.
|
||||
*/
|
||||
val navigationSerializersModule: SerializersModule =
|
||||
authSerializersModule +
|
||||
chatSerializersModule +
|
||||
conversationsSerializersModule +
|
||||
agentsSerializersModule +
|
||||
filesSerializersModule +
|
||||
settingsSerializersModule
|
||||
|
|
@ -37,29 +37,43 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import kotlin.reflect.KClass
|
||||
import com.garfiec.librechat.MainActivity
|
||||
import com.garfiec.librechat.core.ui.components.BannerDisplay
|
||||
import com.garfiec.librechat.feature.agents.navigation.AGENT_EDITOR_CREATE_ROUTE
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentDetail
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentEditorCreate
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentEditorEdit
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentMarketplace
|
||||
import com.garfiec.librechat.feature.agents.navigation.agentsGraph
|
||||
import com.garfiec.librechat.feature.auth.navigation.AUTH_GRAPH_ROUTE
|
||||
import com.garfiec.librechat.feature.auth.navigation.AuthRoute
|
||||
import com.garfiec.librechat.feature.auth.navigation.ServerUrl
|
||||
import com.garfiec.librechat.feature.auth.navigation.authGraph
|
||||
import com.garfiec.librechat.feature.chat.navigation.CHAT_GRAPH_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.NEW_CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.Chat
|
||||
import com.garfiec.librechat.feature.chat.navigation.ChatRoute
|
||||
import com.garfiec.librechat.feature.chat.navigation.NewChat
|
||||
import com.garfiec.librechat.feature.chat.navigation.chatGraph
|
||||
import com.garfiec.librechat.feature.chat.navigation.navigateToChat
|
||||
import com.garfiec.librechat.feature.conversations.navigation.ArchivedConversations
|
||||
import com.garfiec.librechat.feature.conversations.navigation.conversationsGraph
|
||||
import com.garfiec.librechat.feature.files.navigation.Files
|
||||
import com.garfiec.librechat.feature.files.navigation.filesGraph
|
||||
import com.garfiec.librechat.feature.settings.navigation.API_KEYS_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.PRESET_MANAGER_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_TABBED_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SHARED_LINKS_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.ApiKeys
|
||||
import com.garfiec.librechat.feature.settings.navigation.PresetManager
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsTabbed
|
||||
import com.garfiec.librechat.feature.settings.navigation.SharedLinks
|
||||
import com.garfiec.librechat.feature.settings.navigation.settingsGraph
|
||||
import kotlinx.coroutines.launch
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
/** Checks if this destination's route matches the given typed route class. */
|
||||
private fun NavDestination?.isRoute(routeClass: KClass<*>): Boolean {
|
||||
val qualifiedName = routeClass.qualifiedName ?: return false
|
||||
return this?.route?.startsWith(qualifiedName) == true
|
||||
}
|
||||
|
||||
private val SidebarWidth = 320.dp
|
||||
|
||||
/** Velocity (px/s) above which a fling commits the gesture regardless of position. */
|
||||
|
|
@ -69,7 +83,7 @@ private const val FLING_VELOCITY_THRESHOLD = 800f
|
|||
fun TabletLayout(
|
||||
navController: NavHostController,
|
||||
navHostViewModel: NavHostViewModel,
|
||||
startDestination: String,
|
||||
startDestination: Any,
|
||||
isInAuthFlow: Boolean,
|
||||
deepLinkUri: Uri?,
|
||||
onDeepLinkConsumed: () -> Unit,
|
||||
|
|
@ -122,11 +136,12 @@ fun TabletLayout(
|
|||
// non-chat screen (e.g. settings, agents, files).
|
||||
LaunchedEffect(shareNavigationTrigger) {
|
||||
if (shareNavigationTrigger > 0) {
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
val isOnChatScreen = currentRoute == CHAT_ROUTE || currentRoute == NEW_CHAT_ROUTE
|
||||
val currentDest = navController.currentBackStackEntry?.destination
|
||||
val isOnChatScreen = currentDest.isRoute(Chat::class) ||
|
||||
currentDest.isRoute(NewChat::class)
|
||||
if (!isOnChatScreen) {
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
|
@ -215,10 +230,10 @@ fun TabletLayout(
|
|||
viewModel = navHostViewModel,
|
||||
onNewChat = {
|
||||
// Skip navigation if already on the new chat screen
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
if (currentRoute != NEW_CHAT_ROUTE) {
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
val currentDest = navController.currentBackStackEntry?.destination
|
||||
if (!currentDest.isRoute(NewChat::class)) {
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
|
@ -227,7 +242,7 @@ fun TabletLayout(
|
|||
navController.navigateToChat(conversationId)
|
||||
},
|
||||
onSettingsClick = {
|
||||
navController.navigate(SETTINGS_TABBED_ROUTE) {
|
||||
navController.navigate(SettingsTabbed) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -237,12 +252,12 @@ fun TabletLayout(
|
|||
}
|
||||
},
|
||||
onAgentsClick = {
|
||||
navController.navigate(TopLevelDestination.AGENTS.route) {
|
||||
navController.navigate(AgentMarketplace) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onFilesClick = {
|
||||
navController.navigate(TopLevelDestination.FILES.route) {
|
||||
navController.navigate(Files) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -308,7 +323,7 @@ fun TabletLayout(
|
|||
private fun MainContent(
|
||||
navController: NavHostController,
|
||||
navHostViewModel: NavHostViewModel,
|
||||
startDestination: String,
|
||||
startDestination: Any,
|
||||
isInAuthFlow: Boolean,
|
||||
banners: List<com.garfiec.librechat.core.model.Banner>,
|
||||
dismissedBannerIds: Set<String>,
|
||||
|
|
@ -360,8 +375,8 @@ private fun MainContent(
|
|||
navController = navController,
|
||||
onAuthComplete = {
|
||||
navHostViewModel.onAuthComplete()
|
||||
navController.navigate(CHAT_GRAPH_ROUTE) {
|
||||
popUpTo(AUTH_GRAPH_ROUTE) { inclusive = true }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<AuthRoute> { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -374,7 +389,7 @@ private fun MainContent(
|
|||
navController.navigateToChat(conversationId)
|
||||
},
|
||||
onNavigateToArchived = {
|
||||
navController.navigate("conversations/archived")
|
||||
navController.navigate(ArchivedConversations)
|
||||
},
|
||||
onNavigateBackFromArchived = {
|
||||
navController.popBackStack()
|
||||
|
|
@ -382,38 +397,38 @@ private fun MainContent(
|
|||
)
|
||||
agentsGraph(
|
||||
onAgentClick = { agentId ->
|
||||
navController.navigate("agents/$agentId")
|
||||
navController.navigate(AgentDetail(agentId = agentId))
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
onStartChat = { agentId ->
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onCreateAgent = {
|
||||
navController.navigate(AGENT_EDITOR_CREATE_ROUTE)
|
||||
navController.navigate(AgentEditorCreate)
|
||||
},
|
||||
onEditAgent = { agentId ->
|
||||
navController.navigate("agents/editor/$agentId")
|
||||
navController.navigate(AgentEditorEdit(agentId = agentId))
|
||||
},
|
||||
)
|
||||
filesGraph()
|
||||
settingsGraph(
|
||||
onLogout = {
|
||||
navHostViewModel.logout()
|
||||
navController.navigate(AUTH_GRAPH_ROUTE) {
|
||||
navController.navigate(ServerUrl) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToArchived = {
|
||||
navController.navigate("conversations/archived") {
|
||||
navController.navigate(ArchivedConversations) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateToSharedLinks = {
|
||||
navController.navigate(SHARED_LINKS_ROUTE) {
|
||||
navController.navigate(SharedLinks) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -421,7 +436,7 @@ private fun MainContent(
|
|||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToPresets = {
|
||||
navController.navigate(PRESET_MANAGER_ROUTE) {
|
||||
navController.navigate(PresetManager) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -429,7 +444,7 @@ private fun MainContent(
|
|||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToApiKeys = {
|
||||
navController.navigate(API_KEYS_ROUTE) {
|
||||
navController.navigate(ApiKeys) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
package com.garfiec.librechat.feature.agents.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.garfiec.librechat.feature.agents.screen.AgentDetailScreen
|
||||
import com.garfiec.librechat.feature.agents.screen.AgentEditorScreen
|
||||
import com.garfiec.librechat.feature.agents.screen.AgentMarketplaceScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
const val AGENTS_ROUTE = "agents"
|
||||
const val AGENT_DETAIL_ROUTE = "agents/{agentId}"
|
||||
const val AGENT_EDITOR_CREATE_ROUTE = "agents/editor/create"
|
||||
const val AGENT_EDITOR_EDIT_ROUTE = "agents/editor/{agentId}"
|
||||
@Serializable sealed interface AgentsRoute
|
||||
|
||||
@Serializable data object AgentMarketplace : AgentsRoute
|
||||
@Serializable data class AgentDetail(val agentId: String) : AgentsRoute
|
||||
@Serializable data object AgentEditorCreate : AgentsRoute
|
||||
@Serializable data class AgentEditorEdit(val agentId: String) : AgentsRoute
|
||||
|
||||
fun NavGraphBuilder.agentsGraph(
|
||||
onAgentClick: (String) -> Unit,
|
||||
|
|
@ -20,19 +24,14 @@ fun NavGraphBuilder.agentsGraph(
|
|||
onCreateAgent: () -> Unit,
|
||||
onEditAgent: (String) -> Unit,
|
||||
) {
|
||||
composable(AGENTS_ROUTE) {
|
||||
composable<AgentMarketplace> {
|
||||
AgentMarketplaceScreen(
|
||||
onAgentClick = onAgentClick,
|
||||
onCreateAgent = onCreateAgent,
|
||||
onBack = onBack,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = AGENT_DETAIL_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument("agentId") { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
composable<AgentDetail> {
|
||||
AgentDetailScreen(
|
||||
onBack = onBack,
|
||||
onStartChat = onStartChat,
|
||||
|
|
@ -40,21 +39,25 @@ fun NavGraphBuilder.agentsGraph(
|
|||
onDuplicated = onAgentClick,
|
||||
)
|
||||
}
|
||||
composable(AGENT_EDITOR_CREATE_ROUTE) {
|
||||
composable<AgentEditorCreate> {
|
||||
AgentEditorScreen(
|
||||
onBack = onBack,
|
||||
onSaved = { agentId -> onAgentClick(agentId) },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = AGENT_EDITOR_EDIT_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument("agentId") { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
composable<AgentEditorEdit> {
|
||||
AgentEditorScreen(
|
||||
onBack = onBack,
|
||||
onSaved = { agentId -> onAgentClick(agentId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val agentsSerializersModule = SerializersModule {
|
||||
polymorphic(AgentsRoute::class) {
|
||||
subclass(AgentMarketplace::class, AgentMarketplace.serializer())
|
||||
subclass(AgentDetail::class, AgentDetail.serializer())
|
||||
subclass(AgentEditorCreate::class, AgentEditorCreate.serializer())
|
||||
subclass(AgentEditorEdit::class, AgentEditorEdit.serializer())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ package com.garfiec.librechat.feature.auth.navigation
|
|||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navigation
|
||||
import com.garfiec.librechat.core.ui.components.ScreenTransitionWrapper
|
||||
import com.garfiec.librechat.feature.auth.screen.ForgotPasswordScreen
|
||||
|
|
@ -15,84 +13,70 @@ import com.garfiec.librechat.feature.auth.screen.ServerUrlScreen
|
|||
import com.garfiec.librechat.feature.auth.screen.TermsScreen
|
||||
import com.garfiec.librechat.feature.auth.screen.TwoFactorScreen
|
||||
import com.garfiec.librechat.feature.auth.screen.VerifyEmailScreen
|
||||
const val AUTH_GRAPH_ROUTE = "auth_graph"
|
||||
const val SERVER_URL_ROUTE = "server_url"
|
||||
const val LOGIN_ROUTE = "login"
|
||||
const val REGISTER_ROUTE = "register"
|
||||
const val FORGOT_PASSWORD_ROUTE = "forgot_password"
|
||||
const val TWO_FACTOR_ROUTE = "two_factor/{tempToken}"
|
||||
const val VERIFY_EMAIL_ROUTE = "verify_email/{email}"
|
||||
const val TERMS_ROUTE = "auth/terms"
|
||||
const val RESET_PASSWORD_ROUTE = "reset_password/{userId}/{token}"
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
@Serializable sealed interface AuthRoute
|
||||
|
||||
@Serializable data object ServerUrl : AuthRoute
|
||||
@Serializable data object Login : AuthRoute
|
||||
@Serializable data object Register : AuthRoute
|
||||
@Serializable data object ForgotPassword : AuthRoute
|
||||
@Serializable data class TwoFactor(val tempToken: String) : AuthRoute
|
||||
@Serializable data class VerifyEmail(val email: String) : AuthRoute
|
||||
@Serializable data object Terms : AuthRoute
|
||||
@Serializable data class ResetPassword(val userId: String, val token: String) : AuthRoute
|
||||
|
||||
fun NavController.navigateToVerifyEmail(email: String) {
|
||||
navigate("verify_email/${encodeNavArg(email)}")
|
||||
navigate(VerifyEmail(email = email))
|
||||
}
|
||||
|
||||
fun NavController.navigateToResetPassword(userId: String, token: String) {
|
||||
navigate("reset_password/${encodeNavArg(userId)}/${encodeNavArg(token)}")
|
||||
}
|
||||
|
||||
private fun encodeNavArg(value: String): String = buildString {
|
||||
for (c in value) {
|
||||
when {
|
||||
c.isLetterOrDigit() || c in "-._~" -> append(c)
|
||||
else -> {
|
||||
val bytes = c.toString().encodeToByteArray()
|
||||
for (b in bytes) {
|
||||
append('%')
|
||||
append(
|
||||
(b.toInt() and 0xFF).toString(16).uppercase().padStart(2, '0'),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
navigate(ResetPassword(userId = userId, token = token))
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.authGraph(
|
||||
navController: NavController,
|
||||
onAuthComplete: () -> Unit,
|
||||
) {
|
||||
navigation(startDestination = SERVER_URL_ROUTE, route = AUTH_GRAPH_ROUTE) {
|
||||
composable(SERVER_URL_ROUTE) {
|
||||
navigation<AuthRoute>(startDestination = ServerUrl::class) {
|
||||
composable<ServerUrl> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
ServerUrlScreen(
|
||||
onServerValidated = { navController.navigate(LOGIN_ROUTE) },
|
||||
onServerValidated = { navController.navigate(Login) },
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(LOGIN_ROUTE) {
|
||||
composable<Login> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
LoginScreen(
|
||||
onLoginSuccess = onAuthComplete,
|
||||
onNavigateToRegister = { navController.navigate(REGISTER_ROUTE) },
|
||||
onNavigateToForgotPassword = { navController.navigate(FORGOT_PASSWORD_ROUTE) },
|
||||
onNavigateToRegister = { navController.navigate(Register) },
|
||||
onNavigateToForgotPassword = { navController.navigate(ForgotPassword) },
|
||||
onNavigateToTwoFactor = { tempToken ->
|
||||
navController.navigate("two_factor/$tempToken")
|
||||
navController.navigate(TwoFactor(tempToken = tempToken))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(REGISTER_ROUTE) {
|
||||
composable<Register> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
RegisterScreen(
|
||||
onRegistered = { navController.popBackStack(LOGIN_ROUTE, inclusive = false) },
|
||||
onRegistered = { navController.popBackStack<Login>(inclusive = false) },
|
||||
onNavigateToLogin = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(FORGOT_PASSWORD_ROUTE) {
|
||||
composable<ForgotPassword> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
ForgotPasswordScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = TWO_FACTOR_ROUTE,
|
||||
arguments = listOf(navArgument("tempToken") { type = NavType.StringType }),
|
||||
) {
|
||||
composable<TwoFactor> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
TwoFactorScreen(
|
||||
onVerified = onAuthComplete,
|
||||
|
|
@ -100,18 +84,15 @@ fun NavGraphBuilder.authGraph(
|
|||
)
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = VERIFY_EMAIL_ROUTE,
|
||||
arguments = listOf(navArgument("email") { type = NavType.StringType }),
|
||||
) {
|
||||
composable<VerifyEmail> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
VerifyEmailScreen(
|
||||
onVerified = { navController.popBackStack(LOGIN_ROUTE, inclusive = false) },
|
||||
onVerified = { navController.popBackStack<Login>(inclusive = false) },
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(TERMS_ROUTE) {
|
||||
composable<Terms> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
TermsScreen(
|
||||
onAccepted = onAuthComplete,
|
||||
|
|
@ -119,19 +100,26 @@ fun NavGraphBuilder.authGraph(
|
|||
)
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = RESET_PASSWORD_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument("userId") { type = NavType.StringType },
|
||||
navArgument("token") { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
composable<ResetPassword> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
ResetPasswordScreen(
|
||||
onResetComplete = { navController.popBackStack(LOGIN_ROUTE, inclusive = false) },
|
||||
onResetComplete = { navController.popBackStack<Login>(inclusive = false) },
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val authSerializersModule = SerializersModule {
|
||||
polymorphic(AuthRoute::class) {
|
||||
subclass(ServerUrl::class, ServerUrl.serializer())
|
||||
subclass(Login::class, Login.serializer())
|
||||
subclass(Register::class, Register.serializer())
|
||||
subclass(ForgotPassword::class, ForgotPassword.serializer())
|
||||
subclass(TwoFactor::class, TwoFactor.serializer())
|
||||
subclass(VerifyEmail::class, VerifyEmail.serializer())
|
||||
subclass(Terms::class, Terms.serializer())
|
||||
subclass(ResetPassword::class, ResetPassword.serializer())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,27 +3,38 @@ package com.garfiec.librechat.feature.chat.navigation
|
|||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navigation
|
||||
import kotlin.reflect.KClass
|
||||
import com.garfiec.librechat.feature.chat.prompts.PromptEditorScreen
|
||||
import com.garfiec.librechat.feature.chat.prompts.PromptsLibraryScreen
|
||||
import com.garfiec.librechat.feature.chat.screen.ChatScreen
|
||||
import com.garfiec.librechat.feature.chat.screen.NewChatScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
const val CHAT_GRAPH_ROUTE = "chat_graph"
|
||||
const val NEW_CHAT_ROUTE = "new_chat"
|
||||
const val CHAT_ROUTE = "chat/{conversationId}"
|
||||
const val PROMPTS_LIBRARY_ROUTE = "prompts_library"
|
||||
const val PROMPT_EDITOR_ROUTE = "prompt_editor?groupId={groupId}"
|
||||
@Serializable sealed interface ChatRoute
|
||||
|
||||
@Serializable data object NewChat : ChatRoute
|
||||
@Serializable data class Chat(val conversationId: String? = null) : ChatRoute
|
||||
@Serializable data object PromptsLibrary : ChatRoute
|
||||
@Serializable data class PromptEditor(val groupId: String? = null) : ChatRoute
|
||||
|
||||
/** Checks if this destination's route matches the given typed route class. */
|
||||
internal fun NavDestination?.isRoute(routeClass: KClass<*>): Boolean {
|
||||
val qualifiedName = routeClass.qualifiedName ?: return false
|
||||
return this?.route?.startsWith(qualifiedName) == true
|
||||
}
|
||||
|
||||
fun NavController.navigateToChat(conversationId: String) {
|
||||
val currentEntry = currentBackStackEntry
|
||||
val isCurrentlyInChat = currentEntry?.destination?.route == CHAT_ROUTE
|
||||
val isCurrentlyInChat = currentEntry?.destination.isRoute(Chat::class)
|
||||
val currentDestId = currentEntry?.destination?.id
|
||||
navigate("chat/$conversationId") {
|
||||
navigate(Chat(conversationId = conversationId)) {
|
||||
if (isCurrentlyInChat && currentDestId != null) {
|
||||
// Already viewing a chat -- replace it so switching chats
|
||||
// doesn't stack entries. Back will go to whatever was
|
||||
|
|
@ -36,24 +47,19 @@ fun NavController.navigateToChat(conversationId: String) {
|
|||
}
|
||||
|
||||
fun NavController.navigateToPromptsLibrary() {
|
||||
navigate(PROMPTS_LIBRARY_ROUTE)
|
||||
navigate(PromptsLibrary)
|
||||
}
|
||||
|
||||
fun NavController.navigateToPromptEditor(groupId: String? = null) {
|
||||
if (groupId != null) {
|
||||
navigate("prompt_editor?groupId=$groupId")
|
||||
} else {
|
||||
navigate("prompt_editor")
|
||||
}
|
||||
navigate(PromptEditor(groupId = groupId))
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.chatGraph(
|
||||
navController: NavController,
|
||||
onOpenDrawer: (() -> Unit)? = null,
|
||||
) {
|
||||
navigation(startDestination = NEW_CHAT_ROUTE, route = CHAT_GRAPH_ROUTE) {
|
||||
composable(
|
||||
route = NEW_CHAT_ROUTE,
|
||||
navigation<ChatRoute>(startDestination = NewChat::class) {
|
||||
composable<NewChat>(
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
popEnterTransition = { null },
|
||||
|
|
@ -67,15 +73,7 @@ fun NavGraphBuilder.chatGraph(
|
|||
onNavigateToPromptsLibrary = { navController.navigateToPromptsLibrary() },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = CHAT_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument("conversationId") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
),
|
||||
composable<Chat>(
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
popEnterTransition = { null },
|
||||
|
|
@ -88,7 +86,7 @@ fun NavGraphBuilder.chatGraph(
|
|||
onNavigateToConversation = { conversationId -> navController.navigateToChat(conversationId) },
|
||||
)
|
||||
}
|
||||
composable(PROMPTS_LIBRARY_ROUTE) {
|
||||
composable<PromptsLibrary> {
|
||||
PromptsLibraryScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onUseInChat = { promptText ->
|
||||
|
|
@ -99,19 +97,19 @@ fun NavGraphBuilder.chatGraph(
|
|||
},
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = PROMPT_EDITOR_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument("groupId") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
),
|
||||
) {
|
||||
composable<PromptEditor> {
|
||||
PromptEditorScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chatSerializersModule = SerializersModule {
|
||||
polymorphic(ChatRoute::class) {
|
||||
subclass(NewChat::class, NewChat.serializer())
|
||||
subclass(Chat::class, Chat.serializer())
|
||||
subclass(PromptsLibrary::class, PromptsLibrary.serializer())
|
||||
subclass(PromptEditor::class, PromptEditor.serializer())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,22 @@ import androidx.navigation.compose.composable
|
|||
import com.garfiec.librechat.core.ui.components.ScreenTransitionWrapper
|
||||
import com.garfiec.librechat.feature.conversations.screen.ArchivedConversationsScreen
|
||||
import com.garfiec.librechat.feature.conversations.screen.ConversationListScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
const val CONVERSATIONS_ROUTE = "conversations"
|
||||
const val ARCHIVED_ROUTE = "conversations/archived"
|
||||
@Serializable sealed interface ConversationsRoute
|
||||
|
||||
@Serializable data object Conversations : ConversationsRoute
|
||||
@Serializable data object ArchivedConversations : ConversationsRoute
|
||||
|
||||
fun NavGraphBuilder.conversationsGraph(
|
||||
onConversationClick: (String) -> Unit,
|
||||
onNavigateToArchived: () -> Unit = {},
|
||||
onNavigateBackFromArchived: () -> Unit = {},
|
||||
) {
|
||||
composable(CONVERSATIONS_ROUTE) {
|
||||
composable<Conversations> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
ConversationListScreen(
|
||||
onConversationClick = onConversationClick,
|
||||
|
|
@ -22,7 +28,7 @@ fun NavGraphBuilder.conversationsGraph(
|
|||
)
|
||||
}
|
||||
}
|
||||
composable(ARCHIVED_ROUTE) {
|
||||
composable<ArchivedConversations> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
ArchivedConversationsScreen(
|
||||
onNavigateBack = onNavigateBackFromArchived,
|
||||
|
|
@ -30,3 +36,10 @@ fun NavGraphBuilder.conversationsGraph(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val conversationsSerializersModule = SerializersModule {
|
||||
polymorphic(ConversationsRoute::class) {
|
||||
subclass(Conversations::class, Conversations.serializer())
|
||||
subclass(ArchivedConversations::class, ArchivedConversations.serializer())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,27 @@ import androidx.navigation.NavGraphBuilder
|
|||
import androidx.navigation.compose.composable
|
||||
import com.garfiec.librechat.core.ui.components.ScreenTransitionWrapper
|
||||
import com.garfiec.librechat.feature.files.screen.FilesScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
const val FILES_ROUTE = "files"
|
||||
@Serializable sealed interface FilesRoute
|
||||
|
||||
@Serializable data object Files : FilesRoute
|
||||
|
||||
fun NavGraphBuilder.filesGraph(
|
||||
onBack: (() -> Unit)? = null,
|
||||
) {
|
||||
composable(FILES_ROUTE) {
|
||||
composable<Files> {
|
||||
ScreenTransitionWrapper(transition) {
|
||||
FilesScreen(onBack = onBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val filesSerializersModule = SerializersModule {
|
||||
polymorphic(FilesRoute::class) {
|
||||
subclass(Files::class, Files.serializer())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package com.garfiec.librechat.feature.settings.navigation
|
|||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.garfiec.librechat.feature.settings.screen.McpServersScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
const val MCP_SERVERS_ROUTE = "settings/mcp"
|
||||
@Serializable data object McpServers : SettingsRoute
|
||||
|
||||
fun NavGraphBuilder.mcpServersScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composable(MCP_SERVERS_ROUTE) {
|
||||
composable<McpServers> {
|
||||
McpServersScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package com.garfiec.librechat.feature.settings.navigation
|
|||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.garfiec.librechat.feature.settings.screen.MemoriesScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
const val MEMORIES_ROUTE = "settings/memories"
|
||||
@Serializable data object Memories : SettingsRoute
|
||||
|
||||
fun NavGraphBuilder.memoriesScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composable(MEMORIES_ROUTE) {
|
||||
composable<Memories> {
|
||||
MemoriesScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,16 +13,22 @@ import com.garfiec.librechat.feature.settings.screen.PresetManagerScreen
|
|||
import com.garfiec.librechat.feature.settings.screen.SharedLinksScreen
|
||||
import com.garfiec.librechat.feature.settings.screen.TabbedSettingsScreen
|
||||
import com.garfiec.librechat.feature.settings.viewmodel.SettingsViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
const val SETTINGS_TABBED_ROUTE = "settings"
|
||||
const val SETTINGS_GENERAL_ROUTE = "settings/general"
|
||||
const val SETTINGS_CHAT_ROUTE = "settings/chat"
|
||||
const val SETTINGS_ACCOUNT_ROUTE = "settings/account"
|
||||
const val SETTINGS_DATA_ROUTE = "settings/data"
|
||||
const val SHARED_LINKS_ROUTE = "settings/shared-links"
|
||||
const val PRESET_MANAGER_ROUTE = "settings/presets"
|
||||
const val API_KEYS_ROUTE = "settings/api_keys"
|
||||
@Serializable sealed interface SettingsRoute
|
||||
|
||||
@Serializable data object SettingsTabbed : SettingsRoute
|
||||
@Serializable data object SettingsGeneral : SettingsRoute
|
||||
@Serializable data object SettingsChat : SettingsRoute
|
||||
@Serializable data object SettingsAccount : SettingsRoute
|
||||
@Serializable data object SettingsData : SettingsRoute
|
||||
@Serializable data object SharedLinks : SettingsRoute
|
||||
@Serializable data object PresetManager : SettingsRoute
|
||||
@Serializable data object ApiKeys : SettingsRoute
|
||||
|
||||
fun NavGraphBuilder.settingsGraph(
|
||||
onLogout: () -> Unit,
|
||||
|
|
@ -35,7 +41,7 @@ fun NavGraphBuilder.settingsGraph(
|
|||
onNavigateToApiKeys: () -> Unit = {},
|
||||
onNavigateBackFromApiKeys: () -> Unit = {},
|
||||
) {
|
||||
composable(SETTINGS_TABBED_ROUTE) {
|
||||
composable<SettingsTabbed> {
|
||||
TabbedSettingsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onLogout = onLogout,
|
||||
|
|
@ -46,32 +52,32 @@ fun NavGraphBuilder.settingsGraph(
|
|||
)
|
||||
}
|
||||
|
||||
composable(SETTINGS_GENERAL_ROUTE) {
|
||||
composable<SettingsGeneral> {
|
||||
GeneralSettingsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
composable(SETTINGS_CHAT_ROUTE) {
|
||||
composable<SettingsChat> {
|
||||
ChatSettingsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToPresets = onNavigateToPresets,
|
||||
)
|
||||
}
|
||||
composable(SETTINGS_ACCOUNT_ROUTE) {
|
||||
composable<SettingsAccount> {
|
||||
AccountSettingsScreen(
|
||||
onLogout = onLogout,
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToApiKeys = onNavigateToApiKeys,
|
||||
)
|
||||
}
|
||||
composable(SETTINGS_DATA_ROUTE) {
|
||||
composable<SettingsData> {
|
||||
DataSettingsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToArchived = onNavigateToArchived,
|
||||
onNavigateToSharedLinks = onNavigateToSharedLinks,
|
||||
)
|
||||
}
|
||||
composable(SHARED_LINKS_ROUTE) {
|
||||
composable<SharedLinks> {
|
||||
val viewModel: SettingsViewModel = koinViewModel()
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
|
|
@ -90,14 +96,29 @@ fun NavGraphBuilder.settingsGraph(
|
|||
onNavigateBack = onNavigateBackFromSharedLinks,
|
||||
)
|
||||
}
|
||||
composable(PRESET_MANAGER_ROUTE) {
|
||||
composable<PresetManager> {
|
||||
PresetManagerScreen(
|
||||
onNavigateBack = onNavigateBackFromPresets,
|
||||
)
|
||||
}
|
||||
composable(API_KEYS_ROUTE) {
|
||||
composable<ApiKeys> {
|
||||
ApiKeysScreen(
|
||||
onNavigateBack = onNavigateBackFromApiKeys,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val settingsSerializersModule = SerializersModule {
|
||||
polymorphic(SettingsRoute::class) {
|
||||
subclass(SettingsTabbed::class, SettingsTabbed.serializer())
|
||||
subclass(SettingsGeneral::class, SettingsGeneral.serializer())
|
||||
subclass(SettingsChat::class, SettingsChat.serializer())
|
||||
subclass(SettingsAccount::class, SettingsAccount.serializer())
|
||||
subclass(SettingsData::class, SettingsData.serializer())
|
||||
subclass(SharedLinks::class, SharedLinks.serializer())
|
||||
subclass(PresetManager::class, PresetManager.serializer())
|
||||
subclass(ApiKeys::class, ApiKeys.serializer())
|
||||
subclass(Memories::class, Memories.serializer())
|
||||
subclass(McpServers::class, McpServers.serializer())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.garfiec.librechat.shared.navigation
|
||||
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val sharedAppModule = module {
|
||||
viewModelOf(::NavHostViewModel)
|
||||
single<SerializersModule>(named("navigation")) { navigationSerializersModule }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,30 +29,39 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.savedstate.read
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.toRoute
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlin.reflect.KClass
|
||||
import com.garfiec.librechat.core.ui.components.BannerDisplay
|
||||
import com.garfiec.librechat.feature.agents.navigation.AGENT_EDITOR_CREATE_ROUTE
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentDetail
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentEditorCreate
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentEditorEdit
|
||||
import com.garfiec.librechat.feature.agents.navigation.AgentMarketplace
|
||||
import com.garfiec.librechat.feature.agents.navigation.agentsGraph
|
||||
import com.garfiec.librechat.feature.auth.navigation.AUTH_GRAPH_ROUTE
|
||||
import com.garfiec.librechat.feature.auth.navigation.AuthRoute
|
||||
import com.garfiec.librechat.feature.auth.navigation.ServerUrl
|
||||
import com.garfiec.librechat.feature.auth.navigation.authGraph
|
||||
import com.garfiec.librechat.feature.chat.navigation.CHAT_GRAPH_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.NEW_CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.chat.navigation.Chat
|
||||
import com.garfiec.librechat.feature.chat.navigation.ChatRoute
|
||||
import com.garfiec.librechat.feature.chat.navigation.NewChat
|
||||
import com.garfiec.librechat.feature.chat.navigation.chatGraph
|
||||
import com.garfiec.librechat.feature.chat.navigation.navigateToChat
|
||||
import com.garfiec.librechat.feature.conversations.navigation.ArchivedConversations
|
||||
import com.garfiec.librechat.feature.conversations.navigation.conversationsGraph
|
||||
import com.garfiec.librechat.feature.files.navigation.Files
|
||||
import com.garfiec.librechat.feature.files.navigation.filesGraph
|
||||
import com.garfiec.librechat.feature.settings.navigation.API_KEYS_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.PRESET_MANAGER_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_ACCOUNT_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_CHAT_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_DATA_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_GENERAL_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SETTINGS_TABBED_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.SHARED_LINKS_ROUTE
|
||||
import com.garfiec.librechat.feature.settings.navigation.ApiKeys
|
||||
import com.garfiec.librechat.feature.settings.navigation.PresetManager
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsAccount
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsChat
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsData
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsGeneral
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsRoute
|
||||
import com.garfiec.librechat.feature.settings.navigation.SettingsTabbed
|
||||
import com.garfiec.librechat.feature.settings.navigation.SharedLinks
|
||||
import com.garfiec.librechat.feature.settings.navigation.settingsGraph
|
||||
import kotlinx.coroutines.launch
|
||||
import librechat_mobile.shared.generated.resources.Res
|
||||
|
|
@ -63,12 +72,18 @@ import librechat_mobile.shared.generated.resources.version_mismatch_title
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
/** Maps a [SettingsCategory] to its corresponding navigation route. */
|
||||
fun SettingsCategory.toRoute(): String = when (this) {
|
||||
SettingsCategory.GENERAL -> SETTINGS_GENERAL_ROUTE
|
||||
SettingsCategory.CHAT -> SETTINGS_CHAT_ROUTE
|
||||
SettingsCategory.ACCOUNT -> SETTINGS_ACCOUNT_ROUTE
|
||||
SettingsCategory.DATA -> SETTINGS_DATA_ROUTE
|
||||
/** Checks if this destination's route matches the given typed route class. */
|
||||
private fun NavDestination?.isRoute(routeClass: KClass<*>): Boolean {
|
||||
val qualifiedName = routeClass.qualifiedName ?: return false
|
||||
return this?.route?.startsWith(qualifiedName) == true
|
||||
}
|
||||
|
||||
/** Maps a [SettingsCategory] to its corresponding typed navigation route. */
|
||||
fun SettingsCategory.toRoute(): SettingsRoute = when (this) {
|
||||
SettingsCategory.GENERAL -> SettingsGeneral
|
||||
SettingsCategory.CHAT -> SettingsChat
|
||||
SettingsCategory.ACCOUNT -> SettingsAccount
|
||||
SettingsCategory.DATA -> SettingsData
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,15 +105,13 @@ fun LibreChatNavHost(
|
|||
|
||||
val isLoggedIn by navHostViewModel.isLoggedIn.collectAsStateWithLifecycle()
|
||||
|
||||
val isInAuthFlow = currentDestination?.route == AUTH_GRAPH_ROUTE ||
|
||||
currentDestination?.parent?.route == AUTH_GRAPH_ROUTE
|
||||
val isInAuthFlow = currentDestination.isRoute(AuthRoute::class) ||
|
||||
currentDestination?.parent.isRoute(AuthRoute::class)
|
||||
|
||||
// Track active conversation from nav back stack.
|
||||
// Use SavedState.read {} to extract nav args (KMP-compatible, no Bundle.getString).
|
||||
// Track active conversation from nav back stack
|
||||
LaunchedEffect(navBackStackEntry) {
|
||||
val destRoute = navBackStackEntry?.destination?.route
|
||||
val conversationId = if (destRoute == CHAT_ROUTE) {
|
||||
navBackStackEntry?.arguments?.read { getStringOrNull("conversationId") }
|
||||
val conversationId = if (navBackStackEntry?.destination.isRoute(Chat::class)) {
|
||||
navBackStackEntry?.toRoute<Chat>()?.conversationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -108,13 +121,13 @@ fun LibreChatNavHost(
|
|||
// Handle session expiry
|
||||
LaunchedEffect(Unit) {
|
||||
navHostViewModel.sessionExpired.collect {
|
||||
navController.navigate(AUTH_GRAPH_ROUTE) {
|
||||
navController.navigate(ServerUrl) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val startDestination = if (isLoggedIn) CHAT_GRAPH_ROUTE else AUTH_GRAPH_ROUTE
|
||||
val startDestination: Any = if (isLoggedIn) ChatRoute::class else AuthRoute::class
|
||||
|
||||
PhoneLayout(
|
||||
navController = navController,
|
||||
|
|
@ -174,7 +187,7 @@ private fun VersionMismatchDialog(
|
|||
private fun PhoneLayout(
|
||||
navController: androidx.navigation.NavHostController,
|
||||
navHostViewModel: NavHostViewModel,
|
||||
startDestination: String,
|
||||
startDestination: Any,
|
||||
isInAuthFlow: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -199,10 +212,10 @@ private fun PhoneLayout(
|
|||
viewModel = navHostViewModel,
|
||||
onNewChat = {
|
||||
scope.launch { drawerState.close() }
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
if (currentRoute != NEW_CHAT_ROUTE) {
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
val currentRoute = navController.currentBackStackEntry?.destination
|
||||
if (!currentRoute.isRoute(NewChat::class)) {
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
|
@ -213,7 +226,7 @@ private fun PhoneLayout(
|
|||
},
|
||||
onSettingsClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
navController.navigate(SETTINGS_TABBED_ROUTE) {
|
||||
navController.navigate(SettingsTabbed) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -224,13 +237,13 @@ private fun PhoneLayout(
|
|||
},
|
||||
onAgentsClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
navController.navigate(TopLevelDestination.AGENTS.route) {
|
||||
navController.navigate(AgentMarketplace) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onFilesClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
navController.navigate(TopLevelDestination.FILES.route) {
|
||||
navController.navigate(Files) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -283,8 +296,8 @@ private fun PhoneLayout(
|
|||
navController = navController,
|
||||
onAuthComplete = {
|
||||
navHostViewModel.onAuthComplete()
|
||||
navController.navigate(CHAT_GRAPH_ROUTE) {
|
||||
popUpTo(AUTH_GRAPH_ROUTE) { inclusive = true }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<AuthRoute> { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -299,7 +312,7 @@ private fun PhoneLayout(
|
|||
navController.navigateToChat(conversationId)
|
||||
},
|
||||
onNavigateToArchived = {
|
||||
navController.navigate("conversations/archived")
|
||||
navController.navigate(ArchivedConversations)
|
||||
},
|
||||
onNavigateBackFromArchived = {
|
||||
navController.popBackStack()
|
||||
|
|
@ -307,20 +320,20 @@ private fun PhoneLayout(
|
|||
)
|
||||
agentsGraph(
|
||||
onAgentClick = { agentId ->
|
||||
navController.navigate("agents/$agentId")
|
||||
navController.navigate(AgentDetail(agentId = agentId))
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
onStartChat = { agentId ->
|
||||
navController.navigate(NEW_CHAT_ROUTE) {
|
||||
popUpTo(CHAT_GRAPH_ROUTE) { inclusive = false }
|
||||
navController.navigate(NewChat) {
|
||||
popUpTo<ChatRoute> { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onCreateAgent = {
|
||||
navController.navigate(AGENT_EDITOR_CREATE_ROUTE)
|
||||
navController.navigate(AgentEditorCreate)
|
||||
},
|
||||
onEditAgent = { agentId ->
|
||||
navController.navigate("agents/editor/$agentId")
|
||||
navController.navigate(AgentEditorEdit(agentId = agentId))
|
||||
},
|
||||
)
|
||||
filesGraph(
|
||||
|
|
@ -329,18 +342,18 @@ private fun PhoneLayout(
|
|||
settingsGraph(
|
||||
onLogout = {
|
||||
navHostViewModel.logout()
|
||||
navController.navigate(AUTH_GRAPH_ROUTE) {
|
||||
navController.navigate(ServerUrl) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToArchived = {
|
||||
navController.navigate("conversations/archived") {
|
||||
navController.navigate(ArchivedConversations) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateToSharedLinks = {
|
||||
navController.navigate(SHARED_LINKS_ROUTE) {
|
||||
navController.navigate(SharedLinks) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -348,7 +361,7 @@ private fun PhoneLayout(
|
|||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToPresets = {
|
||||
navController.navigate(PRESET_MANAGER_ROUTE) {
|
||||
navController.navigate(PresetManager) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
@ -356,7 +369,7 @@ private fun PhoneLayout(
|
|||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToApiKeys = {
|
||||
navController.navigate(API_KEYS_ROUTE) {
|
||||
navController.navigate(ApiKeys) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.garfiec.librechat.shared.navigation
|
||||
|
||||
import com.garfiec.librechat.feature.agents.navigation.agentsSerializersModule
|
||||
import com.garfiec.librechat.feature.auth.navigation.authSerializersModule
|
||||
import com.garfiec.librechat.feature.chat.navigation.chatSerializersModule
|
||||
import com.garfiec.librechat.feature.conversations.navigation.conversationsSerializersModule
|
||||
import com.garfiec.librechat.feature.files.navigation.filesSerializersModule
|
||||
import com.garfiec.librechat.feature.settings.navigation.settingsSerializersModule
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.plus
|
||||
|
||||
/**
|
||||
* Combined [SerializersModule] for all navigation route types.
|
||||
* Registers polymorphic serializers for each feature module's sealed route hierarchy.
|
||||
* This will be used by Nav 3's SavedStateConfiguration for type-safe state saving.
|
||||
*/
|
||||
val navigationSerializersModule: SerializersModule =
|
||||
authSerializersModule +
|
||||
chatSerializersModule +
|
||||
conversationsSerializersModule +
|
||||
agentsSerializersModule +
|
||||
filesSerializersModule +
|
||||
settingsSerializersModule
|
||||
Loading…
Reference in a new issue