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.
428 lines
17 KiB
Kotlin
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() }
|
|
)
|
|
}
|
|
}
|
|
}
|