diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt index 95854a3..a3203da 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt @@ -444,11 +444,8 @@ fun FeedScreen( } } else { // Normal feed: pinned section + swipe actions - // Pinned section header + // Pinned posts (no section header — pin icon on post) if (pinnedPosts.isNotEmpty()) { - item(key = "pinned_header") { - PinnedSectionHeader() - } items(pinnedPosts, key = { "pinned_${it.ghostId ?: "local_${it.localId}"}" }) { post -> SwipeablePostCard( post = post, @@ -479,15 +476,7 @@ fun FeedScreen( snackbarHostState = snackbarHostState ) } - // Separator between pinned and regular posts - if (regularPosts.isNotEmpty()) { - item(key = "pinned_separator") { - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - } - } + // No extra separator — thick dividers built into each post } items(regularPosts, key = { it.ghostId ?: "local_${it.localId}" }) { post -> @@ -1259,340 +1248,296 @@ fun PostCardContent( emptyList() } - Card( + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 6.dp) .combinedClickable( onClick = onClick, onClickLabel = "View post details", - onLongClick = { - showContextMenu = true - } - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - colors = CardDefaults.cardColors( - containerColor = if (post.featured) - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - else - MaterialTheme.colorScheme.surface - ) + onLongClick = { showContextMenu = true } + ) + .padding(horizontal = 20.dp, vertical = 14.dp) ) { - Box { - Column(modifier = Modifier.padding(16.dp)) { - // Status row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - StatusBadge(post) - if (post.featured) { - Spacer(modifier = Modifier.width(6.dp)) - Icon( - imageVector = Icons.Filled.PushPin, - contentDescription = "Pinned", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.primary - ) - } - } + // Top row: pin icon (if pinned) + timestamp + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (post.featured) { + Icon( + imageVector = Icons.Filled.PushPin, + contentDescription = "Pinned", + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary + ) + } else { + Spacer(modifier = Modifier.width(1.dp)) + } + Text( + text = formatRelativeTime(post.publishedAt ?: post.createdAt), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.height(6.dp)) + + // Content — the star of the show + if (highlightQuery != null && highlightQuery.isNotBlank()) { + HighlightedText( + text = displayText, + query = highlightQuery, + maxLines = if (expanded) Int.MAX_VALUE else 8 + ) + } else { + Text( + text = displayText, + style = MaterialTheme.typography.bodyLarge, + maxLines = if (expanded) Int.MAX_VALUE else 8, + overflow = TextOverflow.Ellipsis + ) + } + + if (!expanded && post.textContent.length > 280) { + Text( + text = "Show more", + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clickable { expanded = true } + .padding(vertical = 4.dp) + ) + } + + // Image grid + if (allImages.isNotEmpty()) { + Spacer(modifier = Modifier.height(10.dp)) + PostImageGrid( + images = allImages, + onImageClick = { index -> + galleryStartIndex = index + showGallery = true + } + ) + if (!post.imageAlt.isNullOrBlank()) { + var showAltPopup by remember { mutableStateOf(false) } + Text( + text = "ALT", + modifier = Modifier + .padding(top = 4.dp) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.8f)) + .clickable { showAltPopup = !showAltPopup } + .padding(horizontal = 6.dp, vertical = 2.dp), + color = MaterialTheme.colorScheme.inverseOnSurface, + fontSize = 11.sp, + fontWeight = FontWeight.Bold + ) + if (showAltPopup) { Text( - text = formatRelativeTime(post.publishedAt ?: post.createdAt), - style = MaterialTheme.typography.labelSmall, + text = post.imageAlt, + modifier = Modifier.padding(top = 4.dp), + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } - - Spacer(modifier = Modifier.height(8.dp)) - - // Content with optional highlighting - if (highlightQuery != null && highlightQuery.isNotBlank()) { - HighlightedText( - text = displayText, - query = highlightQuery, - maxLines = if (expanded) Int.MAX_VALUE else 8 - ) - } else { - Text( - text = displayText, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (expanded) Int.MAX_VALUE else 8, - overflow = TextOverflow.Ellipsis - ) - } - - if (!expanded && post.textContent.length > 280) { - TextButton( - onClick = { expanded = true }, - contentPadding = PaddingValues(0.dp) - ) { - Text("Show more", style = MaterialTheme.typography.labelMedium) - } - } - - // Image grid (multi-image support) - if (allImages.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) - PostImageGrid( - images = allImages, - onImageClick = { index -> - galleryStartIndex = index - showGallery = true - } - ) - // Alt text badge - if (!post.imageAlt.isNullOrBlank()) { - var showAltPopup by remember { mutableStateOf(false) } - Text( - text = "ALT", - modifier = Modifier - .padding(top = 4.dp) - .clip(RoundedCornerShape(4.dp)) - .background( - MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.8f) - ) - .clickable { showAltPopup = !showAltPopup } - .padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.inverseOnSurface, - fontSize = 11.sp, - fontWeight = FontWeight.Bold - ) - // Alt text popup - if (showAltPopup) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Text( - text = post.imageAlt, - modifier = Modifier.padding(8.dp), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - // Hashtag chips - if (post.tags.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) - @OptIn(ExperimentalLayoutApi::class) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - post.tags.forEach { tag -> - SuggestionChip( - onClick = { onTagClick(tag) }, - label = { - Text("#$tag", style = MaterialTheme.typography.labelSmall) - }, - colors = SuggestionChipDefaults.suggestionChipColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - labelColor = MaterialTheme.colorScheme.onSecondaryContainer - ), - border = null - ) - } - } - } - - // Link preview - if (post.linkUrl != null && post.linkTitle != null) { - Spacer(modifier = Modifier.height(8.dp)) - OutlinedCard(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(12.dp)) { - if (post.linkImageUrl != null) { - AsyncImage( - model = post.linkImageUrl, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(120.dp) - .clip(MaterialTheme.shapes.small), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.height(8.dp)) - } - Text( - text = post.linkTitle, - style = MaterialTheme.typography.titleSmall, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - if (post.linkDescription != null) { - Text( - text = post.linkDescription, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - } - } - } - - // Queue status - if (post.queueStatus != QueueStatus.NONE) { - Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - val queueLabel = when (post.queueStatus) { - QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED -> "Pending upload" - QueueStatus.UPLOADING -> "Uploading..." - QueueStatus.FAILED -> "Upload failed" - else -> "" - } - AssistChip( - onClick = {}, - label = { Text(queueLabel, style = MaterialTheme.typography.labelSmall) } - ) - Spacer(modifier = Modifier.width(8.dp)) - if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) { - TextButton(onClick = onCancelQueue) { - Text("Cancel", style = MaterialTheme.typography.labelSmall) - } - } - } - } - - // Share button row for published posts - if (isPublished && hasShareableUrl) { - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - IconButton( - onClick = onShare, - modifier = Modifier.size(36.dp) - ) { - Icon( - Icons.Default.Share, - contentDescription = "Share", - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - // Long-press context menu - DropdownMenu( - expanded = showContextMenu, - onDismissRequest = { showContextMenu = false }, - offset = DpOffset(16.dp, 0.dp) - ) { - DropdownMenuItem( - text = { Text("Edit") }, - onClick = { - showContextMenu = false - onEdit() - }, - leadingIcon = { Icon(Icons.Default.Edit, contentDescription = null) } - ) - DropdownMenuItem( - text = { Text(if (post.featured) "Unpin post" else "Pin post") }, - onClick = { - showContextMenu = false - onTogglePin() - }, - leadingIcon = { - Icon( - imageVector = if (post.featured) Icons.Outlined.PushPin else Icons.Filled.PushPin, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - } - ) - if (isPublished && hasShareableUrl) { - DropdownMenuItem( - text = { Text("Copy link") }, - onClick = { - showContextMenu = false - onCopyLink() - snackbarHostState?.let { host -> - coroutineScope.launch { - host.showSnackbar("Link copied to clipboard") - } - } - }, - leadingIcon = { - Icon(Icons.Default.ContentCopy, contentDescription = null) - } - ) - DropdownMenuItem( - text = { Text("Share") }, - onClick = { - showContextMenu = false - onShare() - }, - leadingIcon = { - Icon(Icons.Default.Share, contentDescription = null) - } - ) - } - DropdownMenuItem( - text = { Text("Delete") }, - onClick = { - showContextMenu = false - onDelete() - }, - leadingIcon = { - Icon( - Icons.Default.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } - ) - } - - // Post stats badges - if (post.textContent.isNotBlank()) { - val stats = remember(post.textContent, post.imageUrl, post.linkUrl) { - PostStats.fromFeedPost(post) - } - Spacer(modifier = Modifier.height(6.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Reading time with clock icon - val readingLabel = PostStats.formatReadingTime(stats.readingTimeMinutes) - if (readingLabel.isNotEmpty()) { - Icon( - Icons.Default.AccessTime, - contentDescription = null, - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - Text( - text = readingLabel, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } - // Word count - Text( - text = "${stats.wordCount} words", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } } } + + // Hashtag chips + if (post.tags.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + @OptIn(ExperimentalLayoutApi::class) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + post.tags.forEach { tag -> + Text( + text = "#$tag", + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.clickable { onTagClick(tag) } + ) + } + } + } + + // Link preview + if (post.linkUrl != null && post.linkTitle != null) { + Spacer(modifier = Modifier.height(10.dp)) + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + if (post.linkImageUrl != null) { + AsyncImage( + model = post.linkImageUrl, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .clip(MaterialTheme.shapes.small), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(8.dp)) + } + Text( + text = post.linkTitle, + style = MaterialTheme.typography.titleSmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + if (post.linkDescription != null) { + Text( + text = post.linkDescription, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + + // Queue status + if (post.queueStatus != QueueStatus.NONE) { + Spacer(modifier = Modifier.height(8.dp)) + val queueLabel = when (post.queueStatus) { + QueueStatus.QUEUED_PUBLISH, QueueStatus.QUEUED_SCHEDULED -> "Pending upload" + QueueStatus.UPLOADING -> "Uploading..." + QueueStatus.FAILED -> "Upload failed" + else -> "" + } + Row(verticalAlignment = Alignment.CenterVertically) { + AssistChip( + onClick = {}, + label = { Text(queueLabel, style = MaterialTheme.typography.labelSmall) } + ) + if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) { + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = onCancelQueue) { + Text("Cancel", style = MaterialTheme.typography.labelSmall) + } + } + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + // Metadata line: status dot · reading time + val stats = remember(post.textContent, post.imageUrl, post.linkUrl) { + PostStats.fromFeedPost(post) + } + val statusLabel = when { + post.queueStatus != QueueStatus.NONE -> "Pending" + else -> post.status.replaceFirstChar { it.uppercase() } + } + val statusColor = when { + post.queueStatus != QueueStatus.NONE -> Color(0xFFE65100) + post.status == "published" -> Color(0xFF2E7D32) + post.status == "scheduled" -> Color(0xFF1565C0) + else -> Color(0xFF7B1FA2) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + modifier = Modifier + .size(8.dp) + .clip(RoundedCornerShape(4.dp)) + .background(statusColor) + ) + Text( + text = statusLabel, + style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold), + color = statusColor + ) + Text("·", color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + text = PostStats.formatReadingTime(stats.readingTimeMinutes), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Action bar separator + Spacer(modifier = Modifier.height(10.dp)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + + // Action bar — evenly spaced icons + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + IconButton(onClick = onEdit, modifier = Modifier.size(40.dp)) { + Icon( + Icons.Default.Edit, + contentDescription = "Edit", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (isPublished && hasShareableUrl) { + IconButton(onClick = { + onCopyLink() + snackbarHostState?.let { host -> + coroutineScope.launch { + host.showSnackbar("Link copied") + } + } + }, modifier = Modifier.size(40.dp)) { + Icon( + Icons.Default.ContentCopy, + contentDescription = "Copy link", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton(onClick = onShare, modifier = Modifier.size(40.dp)) { + Icon( + Icons.Default.Share, + contentDescription = "Share", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + IconButton(onClick = onTogglePin, modifier = Modifier.size(40.dp)) { + Icon( + imageVector = if (post.featured) Icons.Filled.PushPin else Icons.Outlined.PushPin, + contentDescription = if (post.featured) "Unpin" else "Pin", + modifier = Modifier.size(20.dp), + tint = if (post.featured) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Context menu (long-press) + DropdownMenu( + expanded = showContextMenu, + onDismissRequest = { showContextMenu = false } + ) { + DropdownMenuItem( + text = { Text("Delete") }, + onClick = { showContextMenu = false; onDelete() }, + leadingIcon = { + Icon(Icons.Default.Delete, null, tint = MaterialTheme.colorScheme.error) + } + ) + } } + // Thick separator between posts + HorizontalDivider( + modifier = Modifier.padding(horizontal = 0.dp), + thickness = 2.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f) + ) + // Gallery viewer if (showGallery && allImages.isNotEmpty()) { FullScreenGallery(