Files
Sharrit/app/src/main/java/com/shaarit/presentation/nav/NavGraph.kt
T
bruno 40944a9a55 feat: add Shaarli account config and 401 logout
Add a "Compte Shaarli" settings section to edit the server URL and
API secret with online re-validation, plus a logout action.

Detect 401 responses from the Shaarli API via SessionManager, clear
stored credentials and return the user to the login screen.
2026-10-07 10:07:23 -04:00

428 lines
17 KiB
Kotlin

package com.shaarit.presentation.nav
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.shaarit.core.network.SessionManager
import com.shaarit.core.storage.TokenManager
import java.net.URLEncoder
sealed class Screen(val route: String) {
object Login : Screen("login")
object Feed : Screen("feed?tag={tag}&collectionId={collectionId}") {
fun createRoute(tag: String? = null, collectionId: Long? = null): String {
val params = mutableListOf<String>()
if (tag != null) {
val encoded = URLEncoder.encode(tag, "UTF-8")
params.add("tag=$encoded")
}
if (collectionId != null) {
params.add("collectionId=$collectionId")
}
return if (params.isEmpty()) {
"feed"
} else {
"feed?" + params.joinToString("&")
}
}
}
object Add : Screen("add?url={url}&title={title}&isShare={isShare}")
object Edit : Screen("edit/{linkId}") {
fun createRoute(linkId: Int): String = "edit/$linkId"
}
object Tags : Screen("tags")
object Collections : Screen("collections")
object Dashboard : Screen("dashboard")
object Settings : Screen("settings")
object Help : Screen("help")
object DeadLinks : Screen("dead_links")
object Pinned : Screen("pinned")
object Reader : Screen("reader/{linkId}") {
fun createRoute(linkId: Int): String = "reader/$linkId"
}
object Reminders : Screen("reminders")
object Todo : Screen("todo")
object TodoDetail : Screen("todoDetail/{linkId}") {
fun createRoute(linkId: Int): String = "todoDetail/$linkId"
}
object Browser : Screen("browser/{encodedUrl}") {
fun createRoute(encodedUrl: String): String = "browser/$encodedUrl"
}
}
@Composable
fun AppNavGraph(
startDestination: String = Screen.Login.route,
sessionManager: SessionManager? = null,
tokenManager: TokenManager? = null,
shareUrl: String? = null,
shareTitle: String? = null,
shareDescription: String? = null,
shareTags: List<String>? = null,
isFileShare: Boolean = false,
initialDeepLink: String? = null,
onPlayAudio: ((com.shaarit.domain.model.ShaarliLink) -> Unit)? = null
) {
val navController = rememberNavController()
val context = LocalContext.current
// On a 401 from the Shaarli API, the stored API secret is stale (it changed server-side).
// Wipe the credentials and send the user back to the login screen.
if (sessionManager != null && tokenManager != null) {
androidx.compose.runtime.LaunchedEffect(sessionManager) {
sessionManager.unauthorized.collect {
tokenManager.clearToken()
tokenManager.clearApiSecret()
val currentRoute = navController.currentBackStackEntry?.destination?.route
if (currentRoute != Screen.Login.route) {
navController.navigate(Screen.Login.route) {
popUpTo(navController.graph.id) { inclusive = true }
launchSingleTop = true
}
}
}
}
}
// If user is already logged in and has share data, navigate directly to Add screen
val hasShareData = shareUrl != null || (isFileShare && shareTitle != null)
androidx.compose.runtime.LaunchedEffect(hasShareData) {
if (hasShareData && startDestination != Screen.Login.route) {
val route = if (isFileShare && shareTitle != null) {
val encodedTitle = URLEncoder.encode(shareTitle, "UTF-8")
val encodedDesc = if (shareDescription != null) URLEncoder.encode(shareDescription, "UTF-8") else ""
val encodedTags = shareTags?.joinToString(",") { URLEncoder.encode(it, "UTF-8") } ?: ""
"add?url=&title=$encodedTitle&isShare=true&isFileShare=true&description=$encodedDesc&tags=$encodedTags"
} else if (shareUrl != null) {
val encodedUrl = URLEncoder.encode(shareUrl, "UTF-8")
val encodedTitle = if (shareTitle != null) URLEncoder.encode(shareTitle, "UTF-8") else ""
"add?url=$encodedUrl&title=$encodedTitle&isShare=true&isFileShare=false&description=&tags="
} else null
route?.let {
navController.navigate(it) {
popUpTo(startDestination) { inclusive = true }
}
}
}
}
NavHost(navController = navController, startDestination = startDestination) {
composable(Screen.Login.route) {
com.shaarit.presentation.auth.LoginScreen(
onLoginSuccess = {
if (isFileShare && shareTitle != null) {
// File share - navigate to add screen with file data
val encodedTitle = URLEncoder.encode(shareTitle, "UTF-8")
val encodedDesc = if (shareDescription != null) URLEncoder.encode(shareDescription, "UTF-8") else ""
val encodedTags = shareTags?.joinToString(",") { URLEncoder.encode(it, "UTF-8") } ?: ""
navController.navigate("add?url=&title=$encodedTitle&isShare=true&isFileShare=true&description=$encodedDesc&tags=$encodedTags") {
popUpTo(Screen.Login.route) { inclusive = true }
}
} else if (shareUrl != null) {
// Use proper URL encoding that handles spaces correctly
val encodedUrl = URLEncoder.encode(shareUrl, "UTF-8")
val encodedTitle =
if (shareTitle != null) {
URLEncoder.encode(shareTitle, "UTF-8")
} else ""
navController.navigate("add?url=$encodedUrl&title=$encodedTitle&isShare=true&isFileShare=false&description=&tags=") {
popUpTo(Screen.Login.route) { inclusive = true }
}
} else if (initialDeepLink != null) {
// Handle deep link after login
navController.navigate(initialDeepLink) {
popUpTo(Screen.Login.route) { inclusive = true }
}
} else {
navController.navigate(Screen.Feed.createRoute()) {
popUpTo(Screen.Login.route) { inclusive = true }
}
}
}
)
}
composable(
route = "feed?tag={tag}&collectionId={collectionId}",
arguments = listOf(
navArgument("tag") {
type = NavType.StringType
nullable = true
defaultValue = null
},
navArgument("collectionId") {
type = NavType.LongType
nullable = false
defaultValue = -1L
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://feed" },
navDeepLink { uriPattern = "shaarit://search" }
)
) { backStackEntry ->
val tag = backStackEntry.arguments?.getString("tag")
val collectionId = backStackEntry.arguments
?.getLong("collectionId")
?.takeIf { it != -1L }
com.shaarit.presentation.feed.FeedScreen(
onNavigateToAdd = { navController.navigate("add?url=&title=&isShare=false") },
onNavigateToEdit = { linkId ->
navController.navigate(Screen.Edit.createRoute(linkId))
},
onNavigateToTags = { navController.navigate(Screen.Tags.route) },
onNavigateToCollections = { navController.navigate(Screen.Collections.route) },
onNavigateToSettings = { navController.navigate(Screen.Settings.route) },
onNavigateToHelp = { navController.navigate(Screen.Help.route) },
onNavigateToDeadLinks = { navController.navigate(Screen.DeadLinks.route) },
onNavigateToPinned = { navController.navigate(Screen.Pinned.route) },
onNavigateToReader = { linkId ->
navController.navigate(Screen.Reader.createRoute(linkId))
},
onNavigateToReminders = { navController.navigate(Screen.Reminders.route) },
onNavigateToTodo = { navController.navigate(Screen.Todo.route) },
onNavigateToTodoDetail = { linkId -> navController.navigate(Screen.TodoDetail.createRoute(linkId)) },
onNavigateToBrowser = { url ->
val encoded = URLEncoder.encode(url, "UTF-8")
navController.navigate(Screen.Browser.createRoute(encoded))
},
onPlayAudio = onPlayAudio,
initialTagFilter = tag,
initialCollectionId = collectionId
)
}
composable(
route = "add?url={url}&title={title}&isShare={isShare}&isFileShare={isFileShare}&description={description}&tags={tags}",
arguments = listOf(
navArgument("url") {
type = NavType.StringType
defaultValue = ""
nullable = true
},
navArgument("title") {
type = NavType.StringType
defaultValue = ""
nullable = true
},
navArgument("isShare") {
type = NavType.BoolType
defaultValue = false
},
navArgument("isFileShare") {
type = NavType.BoolType
defaultValue = false
},
navArgument("description") {
type = NavType.StringType
defaultValue = ""
nullable = true
},
navArgument("tags") {
type = NavType.StringType
defaultValue = ""
nullable = true
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://add" }
)
) { backStackEntry ->
val isShare = backStackEntry.arguments?.getBoolean("isShare") ?: false
com.shaarit.presentation.add.AddLinkScreen(
onNavigateBack = { navController.popBackStack() },
onShareSuccess = if (isShare) {
{ (context as? Activity)?.finish() }
} else null
)
}
composable(
route = "edit/{linkId}",
arguments = listOf(
navArgument("linkId") {
type = NavType.IntType
}
)
) {
com.shaarit.presentation.edit.EditLinkScreen(
onNavigateBack = { navController.popBackStack() }
)
}
composable(
route = Screen.Tags.route,
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://tags" }
)
) {
com.shaarit.presentation.tags.TagsScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToFeedWithTag = { tag ->
navController.navigate(Screen.Feed.createRoute(tag)) {
popUpTo(Screen.Tags.route) { inclusive = true }
}
}
)
}
composable(
route = Screen.Collections.route,
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://collections" }
)
) {
com.shaarit.presentation.collections.CollectionsScreen(
onNavigateBack = { navController.popBackStack() },
onCollectionClick = { collectionId, isSmart, query ->
navController.navigate(
if (isSmart) {
Screen.Feed.createRoute(tag = query)
} else {
Screen.Feed.createRoute(collectionId = collectionId)
}
) {
popUpTo(Screen.Collections.route) { inclusive = true }
}
}
)
}
composable(
route = Screen.Dashboard.route,
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://dashboard" }
)
) {
com.shaarit.presentation.dashboard.DashboardScreen(
onNavigateBack = { navController.popBackStack() }
)
}
composable(
route = Screen.Settings.route
) {
com.shaarit.presentation.settings.SettingsScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToDashboard = { navController.navigate(Screen.Dashboard.route) },
onNavigateToLogin = {
navController.navigate(Screen.Login.route) {
popUpTo(navController.graph.id) { inclusive = true }
launchSingleTop = true
}
}
)
}
composable(
route = Screen.Help.route,
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://help" }
)
) {
com.shaarit.presentation.help.HelpScreen(
onNavigateBack = { navController.popBackStack() }
)
}
composable(
route = Screen.DeadLinks.route
) {
com.shaarit.presentation.deadlinks.DeadLinksScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToEdit = { linkId ->
navController.navigate(Screen.Edit.createRoute(linkId))
}
)
}
composable(
route = Screen.Pinned.route
) {
com.shaarit.presentation.pinned.PinnedScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToEdit = { linkId ->
navController.navigate(Screen.Edit.createRoute(linkId))
}
)
}
composable(
route = "reader/{linkId}",
arguments = listOf(
navArgument("linkId") {
type = NavType.IntType
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://reader/{linkId}" }
)
) {
com.shaarit.presentation.reader.ReaderModeScreen(
onNavigateBack = { navController.popBackStack() }
)
}
composable(
route = Screen.Reminders.route
) {
com.shaarit.presentation.reminders.RemindersScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToReader = { linkId ->
navController.navigate(Screen.Reader.createRoute(linkId))
}
)
}
composable(
route = Screen.Todo.route,
deepLinks = listOf(
navDeepLink { uriPattern = "shaarit://todo" }
)
) {
com.shaarit.presentation.todo.TodoScreen(
onNavigateBack = { navController.popBackStack() }
)
}
composable(
route = Screen.TodoDetail.route,
arguments = listOf(
navArgument("linkId") {
type = NavType.IntType
}
)
) { backStackEntry ->
val linkId = backStackEntry.arguments?.getInt("linkId") ?: 0
com.shaarit.presentation.todo.TodoDetailScreen(
linkId = linkId,
onNavigateBack = { navController.popBackStack() }
)
}
composable(
route = Screen.Browser.route,
arguments = listOf(
navArgument("encodedUrl") {
type = NavType.StringType
}
)
) { backStackEntry ->
val encodedUrl = backStackEntry.arguments?.getString("encodedUrl") ?: ""
com.shaarit.presentation.browser.InternalBrowserScreen(
encodedUrl = encodedUrl,
onNavigateBack = { navController.popBackStack() }
)
}
}
}