feat: Migrate grid view to staggered layout with Google Keep-style cards and enhanced tag interaction
- Replace LazyVerticalGrid with LazyVerticalStaggeredGrid for dynamic height cards - Remove fixed height constraints (heightIn) to allow wrap-content behavior - Add onTagClick callback to GridViewItem for tag filtering support - Increase description maxLines from 3 to 6 for better content preview - Replace tag preview badges with full clickable TagChip components in LazyRow - Standardize action
This commit is contained in:
parent
4bd1b5a6f3
commit
4ed567eb3a
@ -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 =
|
||||
|
||||
@ -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,18 +318,28 @@ 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)
|
||||
// Selection checkbox
|
||||
if (selectionMode) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onItemClick?.invoke() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnail
|
||||
if (!link.thumbnailUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
@ -341,21 +352,22 @@ fun GridViewItem(
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(140.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.height(120.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
}
|
||||
|
||||
// Title with pin indicator
|
||||
// Title
|
||||
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)
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(end = 4.dp, top = 2.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
@ -377,64 +389,39 @@ fun GridViewItem(
|
||||
imageVector = Icons.Default.PushPin,
|
||||
contentDescription = "Épinglé",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(start = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// Description (plain text for scroll performance)
|
||||
// Description
|
||||
if (link.description.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = link.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 3,
|
||||
maxLines = 6,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
// Tags (show only first 2)
|
||||
// Tags - clickable like ListView
|
||||
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
|
||||
)
|
||||
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() }
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Metadata row
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
@ -462,73 +449,77 @@ fun GridViewItem(
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
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(24.dp)
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Lire l'audio",
|
||||
tint = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Pin button
|
||||
IconButton(
|
||||
onClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onTogglePin(link.id)
|
||||
},
|
||||
modifier = Modifier.size(24.dp)
|
||||
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(14.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onViewClick,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Visibility,
|
||||
contentDescription = "View Details",
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
|
||||
modifier = Modifier.size(14.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onEditClick(link.id) },
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Edit",
|
||||
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.7f),
|
||||
modifier = Modifier.size(14.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showDeleteDialog = true },
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f),
|
||||
modifier = Modifier.size(14.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user