diff --git a/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt b/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt index 37e631e..2dc24c0 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt @@ -10,6 +10,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -1437,18 +1440,19 @@ fun FeedScreen( else -> { when (viewStyle) { ViewStyle.GRID -> { - LazyVerticalGrid( - columns = GridCells.Fixed(2), + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(2), modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalItemSpacing = 12.dp ) { items(count = pagingItems.itemCount) { index -> val link = pagingItems[index] if (link != null) { GridViewItem( link = link, + onTagClick = viewModel::onTagClicked, onItemClick = { if (selectionMode) { selectedIds = diff --git a/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt b/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt index 51e7c8f..46f57a4 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt @@ -283,11 +283,12 @@ fun ListViewItem( } /** - * Grid view item - compact cards in a 2-column grid + * Grid view item - Google Keep style staggered cards with wrap-content height */ @Composable fun GridViewItem( link: ShaarliLink, + onTagClick: (String) -> Unit, onLinkClick: (String) -> Unit, onItemClick: (() -> Unit)? = null, onItemLongClick: (() -> Unit)? = null, @@ -317,215 +318,205 @@ fun GridViewItem( GlassCard( modifier = Modifier - .fillMaxWidth() - .heightIn(min = 220.dp), + .fillMaxWidth(), 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 + modifier = Modifier.fillMaxWidth() ) { - 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 + // Selection checkbox + if (selectionMode) { Row( - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End ) { - 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() + Checkbox( + checked = isSelected, + onCheckedChange = { onItemClick?.invoke() } ) } } - 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 - ) - } + // Thumbnail + 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(120.dp) + .padding(bottom = 8.dp) + .clip(RoundedCornerShape(12.dp)) + ) + } + + // Title + Row( + verticalAlignment = Alignment.Top, + modifier = Modifier.fillMaxWidth() + ) { + HealthStatusIcon( + healthStatus = link.healthStatus, + modifier = Modifier + .size(16.dp) + .padding(end = 4.dp, top = 2.dp) + ) + + 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) + .padding(start = 4.dp) + ) + } + } + + // Description + if (link.description.isNotBlank()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = link.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 6, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + + // Tags - clickable like ListView + if (link.tags.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + LazyRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + items(link.tags) { tag -> + TagChip(tag = tag, isSelected = false, onClick = { onTagClick(tag) }) } } + } - // 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) + Spacer(modifier = Modifier.height(8.dp)) + + // Metadata row + 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 + ) + if (hasReminder) { + Icon( + Icons.Default.Alarm, + contentDescription = "Rappel programmé", + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(12.dp) + ) + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Action buttons row - all buttons from other views, with proper touch targets + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (link.isAudioPlayable() && onPlayClick != null) { + IconButton( + onClick = { onPlayClick(link) }, + modifier = Modifier.size(32.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 + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Lire l'audio", + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(18.dp) ) - if (hasReminder) { - Icon( - Icons.Default.Alarm, - contentDescription = "Rappel programmé", - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(12.dp) - ) - } - } - - Row { - if (link.isAudioPlayable() && onPlayClick != null) { - IconButton( - onClick = { onPlayClick(link) }, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Lire l'audio", - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(16.dp) - ) - } - } - // 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) - ) - } } } + 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) + ) + } } } }