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.
This commit is contained in:
@@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import com.shaarit.core.network.SessionManager
|
||||||
import com.shaarit.core.storage.BiometricAuthManager
|
import com.shaarit.core.storage.BiometricAuthManager
|
||||||
import com.shaarit.core.storage.SecurityPreferences
|
import com.shaarit.core.storage.SecurityPreferences
|
||||||
import com.shaarit.presentation.auth.LockScreen
|
import com.shaarit.presentation.auth.LockScreen
|
||||||
@@ -42,6 +43,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
@Inject lateinit var tokenManager: com.shaarit.core.storage.TokenManager
|
@Inject lateinit var tokenManager: com.shaarit.core.storage.TokenManager
|
||||||
@Inject lateinit var securityPreferences: SecurityPreferences
|
@Inject lateinit var securityPreferences: SecurityPreferences
|
||||||
@Inject lateinit var biometricAuthManager: BiometricAuthManager
|
@Inject lateinit var biometricAuthManager: BiometricAuthManager
|
||||||
|
@Inject lateinit var sessionManager: SessionManager
|
||||||
@Inject lateinit var audioHandler: AudioHandler
|
@Inject lateinit var audioHandler: AudioHandler
|
||||||
|
|
||||||
// Start as authenticated — lock only triggers after app goes to background
|
// Start as authenticated — lock only triggers after app goes to background
|
||||||
@@ -100,6 +102,8 @@ class MainActivity : FragmentActivity() {
|
|||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
AppNavGraph(
|
AppNavGraph(
|
||||||
startDestination = startDestination,
|
startDestination = startDestination,
|
||||||
|
sessionManager = sessionManager,
|
||||||
|
tokenManager = tokenManager,
|
||||||
shareUrl = shareData.url,
|
shareUrl = shareData.url,
|
||||||
shareTitle = shareData.title,
|
shareTitle = shareData.title,
|
||||||
shareDescription = shareData.description,
|
shareDescription = shareData.description,
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import javax.inject.Inject
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
class AuthInterceptor @Inject constructor(private val tokenManager: TokenManager) : Interceptor {
|
class AuthInterceptor @Inject constructor(
|
||||||
|
private val tokenManager: TokenManager,
|
||||||
|
private val sessionManager: SessionManager
|
||||||
|
) : Interceptor {
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val original = chain.request()
|
val original = chain.request()
|
||||||
val builder = original.newBuilder()
|
val builder = original.newBuilder()
|
||||||
@@ -19,6 +22,14 @@ class AuthInterceptor @Inject constructor(private val tokenManager: TokenManager
|
|||||||
builder.header("Authorization", "Bearer $jwtToken")
|
builder.header("Authorization", "Bearer $jwtToken")
|
||||||
}
|
}
|
||||||
|
|
||||||
return chain.proceed(builder.build())
|
val response = chain.proceed(builder.build())
|
||||||
|
|
||||||
|
// A 401 means our API secret no longer matches the one on the Shaarli server.
|
||||||
|
// Broadcast the event so the UI can clear credentials and return to the login screen.
|
||||||
|
if (response.code == 401) {
|
||||||
|
sessionManager.onUnauthorized()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.shaarit.core.network
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts session-expired events when the Shaarli API rejects our JWT with a 401.
|
||||||
|
*
|
||||||
|
* The Shaarli API secret can change server-side (e.g. "logout all devices" in the Shaarli web UI
|
||||||
|
* regenerates it), which invalidates every JWT generated locally. UI layers collect [unauthorized]
|
||||||
|
* to clear stored credentials and send the user back to the login screen.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class SessionManager @Inject constructor() {
|
||||||
|
|
||||||
|
private val _unauthorized = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
val unauthorized: SharedFlow<Unit> = _unauthorized.asSharedFlow()
|
||||||
|
|
||||||
|
fun onUnauthorized() {
|
||||||
|
_unauthorized.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import androidx.navigation.navDeepLink
|
import androidx.navigation.navDeepLink
|
||||||
|
import com.shaarit.core.network.SessionManager
|
||||||
|
import com.shaarit.core.storage.TokenManager
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
@@ -60,6 +62,8 @@ sealed class Screen(val route: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppNavGraph(
|
fun AppNavGraph(
|
||||||
startDestination: String = Screen.Login.route,
|
startDestination: String = Screen.Login.route,
|
||||||
|
sessionManager: SessionManager? = null,
|
||||||
|
tokenManager: TokenManager? = null,
|
||||||
shareUrl: String? = null,
|
shareUrl: String? = null,
|
||||||
shareTitle: String? = null,
|
shareTitle: String? = null,
|
||||||
shareDescription: String? = null,
|
shareDescription: String? = null,
|
||||||
@@ -70,7 +74,24 @@ fun AppNavGraph(
|
|||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val context = LocalContext.current
|
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
|
// If user is already logged in and has share data, navigate directly to Add screen
|
||||||
val hasShareData = shareUrl != null || (isFileShare && shareTitle != null)
|
val hasShareData = shareUrl != null || (isFileShare && shareTitle != null)
|
||||||
androidx.compose.runtime.LaunchedEffect(hasShareData) {
|
androidx.compose.runtime.LaunchedEffect(hasShareData) {
|
||||||
@@ -292,7 +313,13 @@ fun AppNavGraph(
|
|||||||
) {
|
) {
|
||||||
com.shaarit.presentation.settings.SettingsScreen(
|
com.shaarit.presentation.settings.SettingsScreen(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToDashboard = { navController.navigate(Screen.Dashboard.route) }
|
onNavigateToDashboard = { navController.navigate(Screen.Dashboard.route) },
|
||||||
|
onNavigateToLogin = {
|
||||||
|
navController.navigate(Screen.Login.route) {
|
||||||
|
popUpTo(navController.graph.id) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import java.util.*
|
|||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToDashboard: () -> Unit,
|
onNavigateToDashboard: () -> Unit,
|
||||||
|
onNavigateToLogin: () -> Unit,
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
themePreferences: ThemePreferences = viewModel.themePreferences
|
themePreferences: ThemePreferences = viewModel.themePreferences
|
||||||
) {
|
) {
|
||||||
@@ -125,6 +126,27 @@ fun SettingsScreen(
|
|||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
// Shaarli Account Section
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Compte Shaarli")
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
ShaarliAccountItem(
|
||||||
|
serverUrl = viewModel.shaarliUrl.collectAsState().value,
|
||||||
|
apiSecret = viewModel.shaarliSecret.collectAsState().value,
|
||||||
|
isConfigured = viewModel.isShaarliAccountConfigured(),
|
||||||
|
isSaving = viewModel.isSavingCredentials.collectAsState().value,
|
||||||
|
onUrlChange = { viewModel.updateShaarliUrl(it) },
|
||||||
|
onSecretChange = { viewModel.updateShaarliSecret(it) },
|
||||||
|
onSave = { viewModel.saveShaarliCredentials() },
|
||||||
|
onLogout = {
|
||||||
|
viewModel.logoutShaarli()
|
||||||
|
onNavigateToLogin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Theme Section
|
// Theme Section
|
||||||
item {
|
item {
|
||||||
SettingsSection(title = "Apparence")
|
SettingsSection(title = "Apparence")
|
||||||
@@ -382,6 +404,168 @@ private fun SettingsSection(title: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShaarliAccountItem(
|
||||||
|
serverUrl: String,
|
||||||
|
apiSecret: String,
|
||||||
|
isConfigured: Boolean,
|
||||||
|
isSaving: Boolean,
|
||||||
|
onUrlChange: (String) -> Unit,
|
||||||
|
onSecretChange: (String) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onLogout: () -> Unit
|
||||||
|
) {
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
var showSecret by remember { mutableStateOf(false) }
|
||||||
|
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { isExpanded = !isExpanded }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cloud,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isConfigured) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = "Serveur Shaarli",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (isConfigured) "\u2713 Connect\u00e9" else "Non connect\u00e9",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (isConfigured) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||||
|
contentDescription = if (isExpanded) "R\u00e9duire" else "D\u00e9velopper",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Modifiez l'URL ou le secret API (Configuration \u2192 S\u00e9curit\u00e9 sur votre instance Shaarli) en cas d'erreur 401, puis sauvegardez.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = serverUrl,
|
||||||
|
onValueChange = onUrlChange,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
label = { Text("URL du serveur") },
|
||||||
|
placeholder = { Text("https://monserveur.com/shaarli") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Uri,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = apiSecret,
|
||||||
|
onValueChange = onSecretChange,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
label = { Text("Secret API") },
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = if (showSecret) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showSecret = !showSecret }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (showSecret) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||||
|
contentDescription = if (showSecret) "Masquer" else "Afficher"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (isConfigured) {
|
||||||
|
TextButton(onClick = { showLogoutDialog = true }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Logout,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Se d\u00e9connecter", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSaving) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||||
|
} else {
|
||||||
|
TextButton(onClick = onSave) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Save,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Sauvegarder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLogoutDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLogoutDialog = false },
|
||||||
|
title = { Text("Se d\u00e9connecter ?") },
|
||||||
|
text = { Text("Le secret API sera effac\u00e9 de cet appareil. Vos favoris locaux sont conserv\u00e9s et vous devrez ressaisir le secret pour vous reconnecter.") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showLogoutDialog = false
|
||||||
|
onLogout()
|
||||||
|
}) {
|
||||||
|
Text("Se d\u00e9connecter", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showLogoutDialog = false }) {
|
||||||
|
Text("Annuler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsItem(
|
private fun SettingsItem(
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import com.shaarit.data.local.dao.LinkDao
|
|||||||
import com.shaarit.data.sync.SyncManager
|
import com.shaarit.data.sync.SyncManager
|
||||||
import com.shaarit.data.sync.SyncState
|
import com.shaarit.data.sync.SyncState
|
||||||
import com.shaarit.data.worker.LinkHealthCheckWorker
|
import com.shaarit.data.worker.LinkHealthCheckWorker
|
||||||
|
import com.shaarit.domain.model.Credentials
|
||||||
|
import com.shaarit.domain.repository.AuthRepository
|
||||||
import com.shaarit.domain.usecase.ClassifyBookmarksUseCase
|
import com.shaarit.domain.usecase.ClassifyBookmarksUseCase
|
||||||
import com.shaarit.ui.theme.ThemePreferences
|
import com.shaarit.ui.theme.ThemePreferences
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -34,6 +36,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
private val linkDao: LinkDao,
|
private val linkDao: LinkDao,
|
||||||
private val classifyBookmarksUseCase: ClassifyBookmarksUseCase,
|
private val classifyBookmarksUseCase: ClassifyBookmarksUseCase,
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
val themePreferences: ThemePreferences,
|
val themePreferences: ThemePreferences,
|
||||||
val securityPreferences: SecurityPreferences,
|
val securityPreferences: SecurityPreferences,
|
||||||
@@ -51,6 +54,15 @@ class SettingsViewModel @Inject constructor(
|
|||||||
private val _geminiApiKey = MutableStateFlow(tokenManager.getGeminiApiKey() ?: "")
|
private val _geminiApiKey = MutableStateFlow(tokenManager.getGeminiApiKey() ?: "")
|
||||||
val geminiApiKey: StateFlow<String> = _geminiApiKey.asStateFlow()
|
val geminiApiKey: StateFlow<String> = _geminiApiKey.asStateFlow()
|
||||||
|
|
||||||
|
private val _shaarliUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "")
|
||||||
|
val shaarliUrl: StateFlow<String> = _shaarliUrl.asStateFlow()
|
||||||
|
|
||||||
|
private val _shaarliSecret = MutableStateFlow(tokenManager.getApiSecret() ?: "")
|
||||||
|
val shaarliSecret: StateFlow<String> = _shaarliSecret.asStateFlow()
|
||||||
|
|
||||||
|
private val _isSavingCredentials = MutableStateFlow(false)
|
||||||
|
val isSavingCredentials: StateFlow<Boolean> = _isSavingCredentials.asStateFlow()
|
||||||
|
|
||||||
// Health Check Statistics
|
// Health Check Statistics
|
||||||
val totalBookmarks: StateFlow<Int> = linkDao.getTotalBookmarksCount()
|
val totalBookmarks: StateFlow<Int> = linkDao.getTotalBookmarksCount()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
||||||
@@ -281,6 +293,53 @@ class SettingsViewModel @Inject constructor(
|
|||||||
fun isGeminiApiKeyConfigured(): Boolean {
|
fun isGeminiApiKeyConfigured(): Boolean {
|
||||||
return !tokenManager.getGeminiApiKey().isNullOrBlank()
|
return !tokenManager.getGeminiApiKey().isNullOrBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateShaarliUrl(url: String) {
|
||||||
|
_shaarliUrl.value = url
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateShaarliSecret(secret: String) {
|
||||||
|
_shaarliSecret.value = secret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-validates the entered URL + API secret against the server (same flow as login).
|
||||||
|
* On failure the stored secret is cleared by the repository, but the base URL is kept
|
||||||
|
* so the login screen can still pre-fill it.
|
||||||
|
*/
|
||||||
|
fun saveShaarliCredentials() {
|
||||||
|
val url = _shaarliUrl.value.trim()
|
||||||
|
val secret = _shaarliSecret.value.trim()
|
||||||
|
if (url.isBlank() || secret.isBlank()) {
|
||||||
|
_uiState.value = _uiState.value.copy(message = "URL et secret API requis")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isSavingCredentials.value = true
|
||||||
|
authRepository.login(Credentials(url, secret), url)
|
||||||
|
.onSuccess {
|
||||||
|
syncManager.syncNow()
|
||||||
|
_uiState.value = _uiState.value.copy(message = "\u2713 Connexion r\u00e9ussie, identifiants Shaarli mis \u00e0 jour")
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
message = "\u00c9chec de la connexion : ${error.message ?: "erreur inconnue"}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_isSavingCredentials.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isShaarliAccountConfigured(): Boolean {
|
||||||
|
return authRepository.isLoggedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the stored API secret (base URL is kept for pre-fill) so the user returns to login. */
|
||||||
|
fun logoutShaarli() {
|
||||||
|
authRepository.logout()
|
||||||
|
_shaarliSecret.value = ""
|
||||||
|
_uiState.value = _uiState.value.copy(message = "D\u00e9connect\u00e9")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SettingsUiState(
|
data class SettingsUiState(
|
||||||
|
|||||||
Reference in New Issue
Block a user