mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: redesign feed to Bold Expressive style - no cards, action bar, thick dividers
This commit is contained in:
parent
4b74a14cbf
commit
59238ff572
1 changed files with 277 additions and 332 deletions
|
|
@ -444,11 +444,8 @@ fun FeedScreen(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal feed: pinned section + swipe actions
|
// Normal feed: pinned section + swipe actions
|
||||||
// Pinned section header
|
// Pinned posts (no section header — pin icon on post)
|
||||||
if (pinnedPosts.isNotEmpty()) {
|
if (pinnedPosts.isNotEmpty()) {
|
||||||
item(key = "pinned_header") {
|
|
||||||
PinnedSectionHeader()
|
|
||||||
}
|
|
||||||
items(pinnedPosts, key = { "pinned_${it.ghostId ?: "local_${it.localId}"}" }) { post ->
|
items(pinnedPosts, key = { "pinned_${it.ghostId ?: "local_${it.localId}"}" }) { post ->
|
||||||
SwipeablePostCard(
|
SwipeablePostCard(
|
||||||
post = post,
|
post = post,
|
||||||
|
|
@ -479,15 +476,7 @@ fun FeedScreen(
|
||||||
snackbarHostState = snackbarHostState
|
snackbarHostState = snackbarHostState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Separator between pinned and regular posts
|
// No extra separator — thick dividers built into each post
|
||||||
if (regularPosts.isNotEmpty()) {
|
|
||||||
item(key = "pinned_separator") {
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items(regularPosts, key = { it.ghostId ?: "local_${it.localId}" }) { post ->
|
items(regularPosts, key = { it.ghostId ?: "local_${it.localId}" }) { post ->
|
||||||
|
|
@ -1259,340 +1248,296 @@ fun PostCardContent(
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 6.dp)
|
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onClickLabel = "View post details",
|
onClickLabel = "View post details",
|
||||||
onLongClick = {
|
onLongClick = { showContextMenu = true }
|
||||||
showContextMenu = true
|
)
|
||||||
}
|
.padding(horizontal = 20.dp, vertical = 14.dp)
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = if (post.featured)
|
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.surface
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Box {
|
// Top row: pin icon (if pinned) + timestamp
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Row(
|
||||||
// Status row
|
modifier = Modifier.fillMaxWidth(),
|
||||||
Row(
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
) {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
if (post.featured) {
|
||||||
) {
|
Icon(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
imageVector = Icons.Filled.PushPin,
|
||||||
StatusBadge(post)
|
contentDescription = "Pinned",
|
||||||
if (post.featured) {
|
modifier = Modifier.size(14.dp),
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
tint = MaterialTheme.colorScheme.primary
|
||||||
Icon(
|
)
|
||||||
imageVector = Icons.Filled.PushPin,
|
} else {
|
||||||
contentDescription = "Pinned",
|
Spacer(modifier = Modifier.width(1.dp))
|
||||||
modifier = Modifier.size(16.dp),
|
}
|
||||||
tint = MaterialTheme.colorScheme.primary
|
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(
|
||||||
text = formatRelativeTime(post.publishedAt ?: post.createdAt),
|
text = post.imageAlt,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
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
|
// Gallery viewer
|
||||||
if (showGallery && allImages.isNotEmpty()) {
|
if (showGallery && allImages.isNotEmpty()) {
|
||||||
FullScreenGallery(
|
FullScreenGallery(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue