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