From 9479986e330673eb94d5602292b995819901ece9 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 23 Feb 2026 09:01:54 -0500 Subject: [PATCH] feat: Implement dead links management screen allowing pagination, selection, deletion, and re-verification. --- .../presentation/deadlinks/DeadLinksScreen.kt | 144 ++++++++++++++---- .../deadlinks/DeadLinksViewModel.kt | 59 +++++++ version.properties | 6 +- 3 files changed, 177 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksScreen.kt b/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksScreen.kt index 9603107..83d48d9 100644 --- a/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksScreen.kt @@ -13,10 +13,12 @@ import androidx.compose.material.icons.filled.BrokenImage import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.animation.animateColorAsState import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -40,6 +42,7 @@ fun DeadLinksScreen( val snackbarHostState = remember { SnackbarHostState() } val selectedLinkIds by viewModel.selectedLinkIds.collectAsState() val isSelectionMode by viewModel.isSelectionMode.collectAsState() + val testResults by viewModel.linkTestResults.collectAsState() Box( modifier = Modifier @@ -145,28 +148,79 @@ fun DeadLinksScreen( // on va devoir le modifier ou wrapper l'item. // Le viewmodel gère la suppression. - DeadLinkItem( - link = link, - isSelected = selectedLinkIds.contains(link.id), - onItemClick = { - if (isSelectionMode) { - viewModel.toggleSelection(link.id) - } else { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link.url)) - context.startActivity(intent) + val testResult = testResults[link.id] + val dismissState = rememberDismissState( + confirmValueChange = { dismissValue -> + when (dismissValue) { + DismissValue.DismissedToStart -> { + viewModel.deleteLink(link.id) + true + } + DismissValue.DismissedToEnd -> { + viewModel.verifyLink(link.id, link.url) + false + } + else -> false + } + } + ) + + SwipeToDismiss( + state = dismissState, + background = { + val direction = dismissState.dismissDirection ?: return@SwipeToDismiss + val color by animateColorAsState( + when (dismissState.targetValue) { + DismissValue.Default -> Color.Transparent + DismissValue.DismissedToEnd -> Color(0xFF10B981) + DismissValue.DismissedToStart -> Color.Red + } + ) + val alignment = when (direction) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + } + val icon = when (direction) { + DismissDirection.StartToEnd -> Icons.Default.Refresh + DismissDirection.EndToStart -> Icons.Default.Delete + } + + Box( + Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + Icon(icon, contentDescription = null, tint = Color.White) } }, - onLongClick = { - viewModel.toggleSelection(link.id) - }, - onLinkClick = { url -> - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) - }, - onEditClick = onNavigateToEdit, - onDeleteClick = { viewModel.deleteLink(link.id) }, - onTagClick = { }, - onViewClick = { } + dismissContent = { + DeadLinkItem( + link = link, + isSelected = selectedLinkIds.contains(link.id), + testResult = testResult, + onItemClick = { + if (isSelectionMode) { + viewModel.toggleSelection(link.id) + } else { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link.url)) + context.startActivity(intent) + } + }, + onLongClick = { + viewModel.toggleSelection(link.id) + }, + onLinkClick = { url -> + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + }, + onEditClick = onNavigateToEdit, + onDeleteClick = { viewModel.deleteLink(link.id) }, + onTagClick = { }, + onViewClick = { } + ) + } ) } } @@ -182,6 +236,7 @@ fun DeadLinksScreen( private fun DeadLinkItem( link: com.shaarit.domain.model.ShaarliLink, isSelected: Boolean, + testResult: LinkTestResult?, onItemClick: () -> Unit, onLongClick: () -> Unit, onLinkClick: (String) -> Unit, @@ -201,14 +256,45 @@ private fun DeadLinkItem( containerColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant ) ) { - ListViewItem( - link = link, - onItemClick = onItemClick, - onLinkClick = onLinkClick, - onEditClick = { onEditClick(link.id) }, - onDeleteClick = { onDeleteClick(link.id) }, - onTagClick = onTagClick, - onViewClick = { onViewClick(link.id) } - ) + Column { + ListViewItem( + link = link, + onItemClick = onItemClick, + onLinkClick = onLinkClick, + onEditClick = { onEditClick(link.id) }, + onDeleteClick = { onDeleteClick(link.id) }, + onTagClick = onTagClick, + onViewClick = { onViewClick(link.id) } + ) + + if (testResult != null) { + when (testResult) { + is LinkTestResult.Loading -> { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + is LinkTestResult.Success -> { + Text( + "Le lien fonctionne ! Statut mis à jour.", + color = Color(0xFF10B981), + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + } + is LinkTestResult.Error -> { + Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text( + "Erreur : ${testResult.message}", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + } + } + } + } + } } } diff --git a/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksViewModel.kt b/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksViewModel.kt index d2b6513..590f18e 100644 --- a/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksViewModel.kt +++ b/app/src/main/java/com/shaarit/presentation/deadlinks/DeadLinksViewModel.kt @@ -65,4 +65,63 @@ class DeadLinksViewModel @Inject constructor( linkRepository.deleteLink(id) } } + + private val _linkTestResults = MutableStateFlow>(emptyMap()) + val linkTestResults: StateFlow> = _linkTestResults.asStateFlow() + + fun verifyLink(id: Int, url: String) { + viewModelScope.launch { + _linkTestResults.value = _linkTestResults.value + (id to LinkTestResult.Loading) + val result = performLinkCheck(url) + if (result is LinkTestResult.Success) { + // Mettre à jour le statut du lien dans la base de données + linkDao.updateLinkHealthStatus( + id = id, + status = com.shaarit.data.local.entity.LinkCheckStatus.VALID, + failCount = 0, + timestamp = System.currentTimeMillis() + ) + } + _linkTestResults.value = _linkTestResults.value + (id to result) + } + } + + private suspend fun performLinkCheck(urlStr: String): LinkTestResult = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + var connection: java.net.HttpURLConnection? = null + try { + val url = java.net.URL(urlStr) + connection = url.openConnection() as java.net.HttpURLConnection + connection.requestMethod = "GET" + connection.connectTimeout = 7000 + connection.readTimeout = 7000 + connection.instanceFollowRedirects = true + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36") + + connection.connect() + val responseCode = connection.responseCode + if (responseCode in 200..399 || responseCode == 401 || responseCode == 403 || responseCode == 405 || responseCode == 429) { + return@withContext LinkTestResult.Success + } else { + return@withContext LinkTestResult.Error("Erreur $responseCode") + } + } catch (e: java.net.UnknownHostException) { + return@withContext LinkTestResult.Error("Erreur DNS") + } catch (e: java.net.SocketTimeoutException) { + return@withContext LinkTestResult.Error("Délai d'attente dépassé") + } catch (e: javax.net.ssl.SSLHandshakeException) { + return@withContext LinkTestResult.Error("Erreur SSL") + } catch (e: java.io.FileNotFoundException) { + return@withContext LinkTestResult.Error("Erreur 404") + } catch (e: Exception) { + return@withContext LinkTestResult.Error(e.message ?: "Erreur inconnue") + } finally { + connection?.disconnect() + } + } +} + +sealed class LinkTestResult { + object Loading : LinkTestResult() + object Success : LinkTestResult() + data class Error(val message: String) : LinkTestResult() } diff --git a/version.properties b/version.properties index ee274a0..129c702 100644 --- a/version.properties +++ b/version.properties @@ -1,3 +1,3 @@ -#Sun Feb 22 14:59:52 2026 -VERSION_NAME=2.5.1 -VERSION_CODE=28 \ No newline at end of file +#Sun Feb 22 21:42:41 2026 +VERSION_NAME=2.7.0 +VERSION_CODE=30 \ No newline at end of file