Bruno Charest ec0931134c docs: Update comprehensive documentation with AI features, health checks, and architecture improvements
- Document automatic token verification on startup and note:// prefix for Markdown notes
- Add extensive AI capabilities section (Gemini integration, auto-tagging, content classification, multi-model fallback)
- Document link health monitoring system with dead link detection and exclusion features
- Add file sharing support (Markdown/text files) and deep links documentation
- Update technology
2026-02-10 21:15:30 -05:00

904 lines
36 KiB
Kotlin

package com.shaarit.presentation.feed
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Warning
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.window.DialogProperties
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.shaarit.domain.model.HealthStatus
import com.shaarit.domain.model.ShaarliLink
import com.shaarit.ui.components.GlassCard
import com.shaarit.ui.components.TagChip
import com.shaarit.ui.theme.Typography
import dev.jeziellago.compose.markdowntext.MarkdownText
import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.graphics.Color
/**
* Full list view item - shows all details including markdown description
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ListViewItem(
link: ShaarliLink,
onTagClick: (String) -> Unit,
onLinkClick: (String) -> Unit,
onItemClick: (() -> Unit)? = null,
onItemLongClick: (() -> Unit)? = null,
selectionMode: Boolean = false,
isSelected: Boolean = false,
onViewClick: () -> Unit,
onEditClick: (Int) -> Unit,
onDeleteClick: () -> Unit,
onTogglePin: (Int) -> Unit = {}
) {
val haptic = LocalHapticFeedback.current
var showDeleteDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) {
DeleteConfirmationDialog(
linkTitle = link.displayTitle,
onConfirm = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onDeleteClick()
showDeleteDialog = false
},
onDismiss = { showDeleteDialog = false }
)
}
GlassCard(
modifier = Modifier.fillMaxWidth(),
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
onLongClick = onItemLongClick,
glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.Top
) {
// Thumbnail (List View)
if (!link.thumbnailUrl.isNullOrBlank()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(link.thumbnailUrl)
.size(200)
.crossfade(true)
.memoryCacheKey(link.thumbnailUrl)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(12.dp))
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = link.displayTitle,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
HealthStatusIcon(
healthStatus = link.healthStatus,
modifier = Modifier.size(16.dp).padding(end = 4.dp)
)
Text(
text = link.url,
style = MaterialTheme.typography.bodySmall,
color = when (link.healthStatus) {
HealthStatus.DEAD -> MaterialTheme.colorScheme.error
HealthStatus.OK -> MaterialTheme.colorScheme.secondary
else -> MaterialTheme.colorScheme.outline
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Row {
if (selectionMode) {
Checkbox(
checked = isSelected,
onCheckedChange = { onItemClick?.invoke() }
)
}
// Pin button
IconButton(
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onTogglePin(link.id)
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.PushPin,
contentDescription = if (link.isPinned) "Désépingler" else "Épingler",
tint = if (link.isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(18.dp)
)
}
IconButton(onClick = onViewClick, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = "View Details",
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp)
)
}
IconButton(onClick = { onEditClick(link.id) }, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Edit",
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp)
)
}
IconButton(onClick = { showDeleteDialog = true }, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp)
)
}
}
}
if (link.description.isNotBlank()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = link.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 5,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
)
}
if (link.tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(link.tags) { tag ->
TagChip(tag = tag, isSelected = false, onClick = { onTagClick(tag) })
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = link.date,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
if (link.isPrivate) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
Icons.Default.Lock,
contentDescription = "Private",
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.size(12.dp)
)
Text(
text = "Private",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
}
}
}
}
}
}
/**
* Grid view item - compact cards in a 2-column grid
*/
@Composable
fun GridViewItem(
link: ShaarliLink,
onLinkClick: (String) -> Unit,
onItemClick: (() -> Unit)? = null,
onItemLongClick: (() -> Unit)? = null,
selectionMode: Boolean = false,
isSelected: Boolean = false,
onViewClick: () -> Unit,
onEditClick: (Int) -> Unit,
onDeleteClick: () -> Unit,
onTogglePin: (Int) -> Unit = {}
) {
val haptic = LocalHapticFeedback.current
var showDeleteDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) {
DeleteConfirmationDialog(
linkTitle = link.displayTitle,
onConfirm = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onDeleteClick()
showDeleteDialog = false
},
onDismiss = { showDeleteDialog = false }
)
}
GlassCard(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 220.dp),
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
onLongClick = onItemLongClick,
glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column {
// Thumbnail (Grid View)
if (!link.thumbnailUrl.isNullOrBlank()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(link.thumbnailUrl)
.size(400, 280)
.crossfade(true)
.memoryCacheKey(link.thumbnailUrl)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(140.dp)
.padding(bottom = 12.dp)
.clip(RoundedCornerShape(12.dp))
)
}
// Title with pin indicator
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
HealthStatusIcon(
healthStatus = link.healthStatus,
modifier = Modifier.size(16.dp).padding(end = 4.dp).align(Alignment.CenterVertically)
)
Text(
text = link.displayTitle,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = when (link.healthStatus) {
HealthStatus.DEAD -> MaterialTheme.colorScheme.error
HealthStatus.OK -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
},
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
if (link.isPinned) {
Icon(
imageVector = Icons.Default.PushPin,
contentDescription = "Épinglé",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp)
)
}
}
Spacer(modifier = Modifier.height(4.dp))
// Description (plain text for scroll performance)
if (link.description.isNotBlank()) {
Text(
text = link.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
)
}
}
Column {
// Tags (show only first 2)
if (link.tags.isNotEmpty()) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(bottom = 8.dp)
) {
link.tags.take(2).forEach { tag ->
Text(
text = "#$tag",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (link.tags.size > 2) {
Text(
text = "+${link.tags.size - 2}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
}
}
}
// Actions row
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
if (selectionMode) {
Checkbox(
checked = isSelected,
onCheckedChange = { onItemClick?.invoke() }
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (link.isPrivate) {
Icon(
Icons.Default.Lock,
contentDescription = "Private",
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.size(12.dp)
)
}
Text(
text = link.date.take(10),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
}
Row {
// Pin button
IconButton(
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onTogglePin(link.id)
},
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.PushPin,
contentDescription = if (link.isPinned) "Désépingler" else "Épingler",
tint = if (link.isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(14.dp)
)
}
IconButton(
onClick = onViewClick,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = "View Details",
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
modifier = Modifier.size(14.dp)
)
}
IconButton(
onClick = { onEditClick(link.id) },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Edit",
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.7f),
modifier = Modifier.size(14.dp)
)
}
IconButton(
onClick = { showDeleteDialog = true },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f),
modifier = Modifier.size(14.dp)
)
}
}
}
}
}
}
}
/**
* Compact view item - minimal info for dense lists
*/
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun CompactViewItem(
link: ShaarliLink,
onLinkClick: (String) -> Unit,
onItemClick: (() -> Unit)? = null,
onItemLongClick: (() -> Unit)? = null,
selectionMode: Boolean = false,
isSelected: Boolean = false,
onViewClick: () -> Unit,
onEditClick: (Int) -> Unit,
onDeleteClick: () -> Unit,
onTogglePin: (Int) -> Unit = {}
) {
val haptic = LocalHapticFeedback.current
var showDeleteDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) {
DeleteConfirmationDialog(
linkTitle = link.displayTitle,
onConfirm = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onDeleteClick()
showDeleteDialog = false
},
onDismiss = { showDeleteDialog = false }
)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.combinedClickable(
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
onLongClick = onItemLongClick
),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
if (selectionMode) {
Checkbox(
checked = isSelected,
onCheckedChange = { onItemClick?.invoke() }
)
Spacer(modifier = Modifier.width(8.dp))
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (link.isPinned) {
Icon(
imageVector = Icons.Default.PushPin,
contentDescription = "Épinglé",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(14.dp)
)
}
if (link.isPrivate) {
Icon(
Icons.Default.Lock,
contentDescription = "Private",
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.size(14.dp)
)
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
HealthStatusIcon(
healthStatus = link.healthStatus,
modifier = Modifier.size(14.dp).padding(end = 4.dp)
)
Text(
text = link.displayTitle,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = when (link.healthStatus) {
HealthStatus.DEAD -> MaterialTheme.colorScheme.error
HealthStatus.OK -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = link.date.take(10),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
if (link.tags.isNotEmpty()) {
Text(
text = link.tags.take(2).joinToString { "#$it" },
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
Row {
IconButton(
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onTogglePin(link.id)
},
modifier = Modifier.size(28.dp)
) {
Icon(
imageVector = Icons.Default.PushPin,
contentDescription = if (link.isPinned) "Désépingler" else "Épingler",
tint = if (link.isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(16.dp)
)
}
IconButton(onClick = onViewClick, modifier = Modifier.size(28.dp)) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = "View Details",
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
modifier = Modifier.size(16.dp)
)
}
IconButton(onClick = { onEditClick(link.id) }, modifier = Modifier.size(28.dp)) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Edit",
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.7f),
modifier = Modifier.size(16.dp)
)
}
IconButton(onClick = { showDeleteDialog = true }, modifier = Modifier.size(28.dp)) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f),
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
/**
* Reusable delete confirmation dialog
*/
@Composable
fun DeleteConfirmationDialog(
linkTitle: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Delete Link", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) },
text = {
Column {
Text("Are you sure you want to delete this link?", color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(modifier = Modifier.height(8.dp))
Text(
linkTitle,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Delete", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel", color = MaterialTheme.colorScheme.outline)
}
},
containerColor = MaterialTheme.colorScheme.surfaceVariant,
titleContentColor = MaterialTheme.colorScheme.onBackground,
textContentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
/**
* Dialog to show full link details
*/
/**
* Full screen link details view (not a dialog window, so we can animate it)
*/
/**
* Full screen link details view (not a dialog window, so we can animate it)
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LinkDetailsView(
link: ShaarliLink,
onDismiss: () -> Unit,
onLinkClick: (String) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable(onClick = onDismiss) // Click outside to dismiss
.background(Color.Black.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
// Stop propagation of clicks to the background
GlassCard(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.fillMaxHeight(0.9f)
.clickable(enabled = false, onClick = {})
) {
Column(
modifier = Modifier
.padding(24.dp)
.fillMaxSize()
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Text(
text = link.displayTitle,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = onDismiss,
modifier = Modifier
.size(32.dp)
.offset(x = 8.dp, y = (-8).dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Hero Image in Details
if (!link.thumbnailUrl.isNullOrBlank()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(link.thumbnailUrl)
.size(800, 400)
.crossfade(true)
.memoryCacheKey(link.thumbnailUrl)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(16.dp))
)
Spacer(modifier = Modifier.height(16.dp))
}
// Scrollable Content
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(androidx.compose.foundation.rememberScrollState())
) {
// URL
Text(
text = link.url,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.clickable { onLinkClick(link.url) }
.padding(vertical = 4.dp)
)
Spacer(modifier = Modifier.height(16.dp))
// Tags
if (link.tags.isNotEmpty()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
link.tags.forEach { tag ->
TagChip(
tag = tag,
isSelected = false,
onClick = { /* No action in dialog */ }
)
}
}
Spacer(modifier = Modifier.height(24.dp))
}
// Metadata
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = link.date,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
if (link.isPrivate) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
Icons.Default.Lock,
contentDescription = "Private",
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.size(14.dp)
)
Text(
text = "Private",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
Spacer(modifier = Modifier.height(24.dp))
// Description
if (link.description.isNotBlank()) {
MarkdownText(
markdown = link.description,
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
modifier = Modifier.fillMaxWidth()
)
} else {
Text(
text = "No description",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
)
}
}
}
}
}
}
/**
* Indicateur visuel du statut de santé d'un lien
* - NOTE: pas d'icône (c'est une note, pas un lien)
* - UNTESTED: icône grise (jamais testé)
* - OK: icône verte (testé et fonctionnel)
* - DEAD: icône rouge (testé et mort)
*/
@Composable
fun HealthStatusIcon(
healthStatus: HealthStatus,
modifier: Modifier = Modifier
) {
when (healthStatus) {
HealthStatus.NOTE -> {
// Pas d'icône pour les notes
}
HealthStatus.UNTESTED -> {
Icon(
imageVector = Icons.Default.HelpOutline,
contentDescription = "Non testé",
tint = MaterialTheme.colorScheme.outline,
modifier = modifier
)
}
HealthStatus.OK -> {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Lien fonctionnel",
tint = Color(0xFF10B981),
modifier = modifier
)
}
HealthStatus.PENDING -> {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "En attente de confirmation",
tint = Color(0xFFFFA726), // Orange
modifier = modifier
)
}
HealthStatus.DEAD -> {
Icon(
imageVector = Icons.Default.BrokenImage,
contentDescription = "Lien mort",
tint = MaterialTheme.colorScheme.error,
modifier = modifier
)
}
}
}