feat: Implement dead links management screen allowing pagination, selection, deletion, and re-verification.
This commit is contained in:
parent
a05f7ce71c
commit
9479986e33
@ -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,9 +148,58 @@ fun DeadLinksScreen(
|
||||
// on va devoir le modifier ou wrapper l'item.
|
||||
// Le viewmodel gère la suppression.
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
dismissContent = {
|
||||
DeadLinkItem(
|
||||
link = link,
|
||||
isSelected = selectedLinkIds.contains(link.id),
|
||||
testResult = testResult,
|
||||
onItemClick = {
|
||||
if (isSelectionMode) {
|
||||
viewModel.toggleSelection(link.id)
|
||||
@ -169,6 +221,8 @@ fun DeadLinksScreen(
|
||||
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,6 +256,7 @@ private fun DeadLinkItem(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
ListViewItem(
|
||||
link = link,
|
||||
onItemClick = onItemClick,
|
||||
@ -210,5 +266,35 @@ private fun DeadLinkItem(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,4 +65,63 @@ class DeadLinksViewModel @Inject constructor(
|
||||
linkRepository.deleteLink(id)
|
||||
}
|
||||
}
|
||||
|
||||
private val _linkTestResults = MutableStateFlow<Map<Int, LinkTestResult>>(emptyMap())
|
||||
val linkTestResults: StateFlow<Map<Int, LinkTestResult>> = _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()
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
#Sun Feb 22 14:59:52 2026
|
||||
VERSION_NAME=2.5.1
|
||||
VERSION_CODE=28
|
||||
#Sun Feb 22 21:42:41 2026
|
||||
VERSION_NAME=2.7.0
|
||||
VERSION_CODE=30
|
||||
Loading…
x
Reference in New Issue
Block a user