feat: add content reveal and animated delete dialog in detail

This commit is contained in:
Paweł Orzech 2026-03-19 14:24:30 +01:00
parent 5183862533
commit 4a7005ce1e
No known key found for this signature in database

View file

@ -6,7 +6,12 @@ import android.content.Context
import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -47,9 +52,12 @@ import com.swoosh.microblog.data.model.FeedPost
import com.swoosh.microblog.data.model.LinkPreview
import com.swoosh.microblog.data.model.PostStats
import com.swoosh.microblog.data.model.QueueStatus
import com.swoosh.microblog.ui.animation.SwooshMotion
import com.swoosh.microblog.ui.components.AnimatedDialog
import com.swoosh.microblog.ui.feed.FullScreenGallery
import com.swoosh.microblog.ui.feed.StatusBadge
import com.swoosh.microblog.ui.feed.formatRelativeTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@ -85,6 +93,16 @@ fun DetailScreen(
emptyList()
}
// D1: Content reveal sequence
val revealCount = 6 // status, text, tags, gallery, link, stats
val sectionVisible = remember { List(revealCount) { mutableStateOf(false) } }
LaunchedEffect(Unit) {
sectionVisible.forEachIndexed { index, state ->
delay(SwooshMotion.RevealDelayMs * index)
state.value = true
}
}
Scaffold(
topBar = {
TopAppBar(
@ -195,140 +213,180 @@ fun DetailScreen(
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Status and time
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
// Section 0 — Status and time
AnimatedVisibility(
visible = sectionVisible[0].value,
enter = fadeIn(SwooshMotion.quick()) + scaleIn(initialScale = 0.8f, animationSpec = SwooshMotion.bouncy())
) {
StatusBadge(post)
Text(
text = formatRelativeTime(post.publishedAt ?: post.createdAt),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(16.dp))
// Full text content
Text(
text = post.textContent,
style = MaterialTheme.typography.bodyLarge
)
// Tags
if (post.tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
post.tags.forEach { tag ->
SuggestionChip(
onClick = {},
label = {
Text("#$tag", style = MaterialTheme.typography.labelMedium)
},
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer
),
border = null
)
}
}
}
// Image gallery
if (allImages.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
DetailImageGallery(
images = allImages,
onImageClick = { index ->
galleryStartIndex = index
showGallery = true
}
)
// Alt text display
if (!post.imageAlt.isNullOrBlank()) {
Spacer(modifier = Modifier.height(6.dp))
StatusBadge(post)
Text(
text = "Alt: ${post.imageAlt}",
style = MaterialTheme.typography.bodySmall.copy(
fontStyle = FontStyle.Italic
),
text = formatRelativeTime(post.publishedAt ?: post.createdAt),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// Link preview
if (post.linkUrl != null) {
Spacer(modifier = Modifier.height(16.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(160.dp)
.clip(MaterialTheme.shapes.small),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(8.dp))
}
if (post.linkTitle != null) {
Text(
text = post.linkTitle,
style = MaterialTheme.typography.titleMedium
Spacer(modifier = Modifier.height(16.dp))
// Section 1 — Full text content
AnimatedVisibility(
visible = sectionVisible[1].value,
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
) {
Text(
text = post.textContent,
style = MaterialTheme.typography.bodyLarge
)
}
// Section 2 — Tags
AnimatedVisibility(
visible = sectionVisible[2].value && post.tags.isNotEmpty(),
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
) {
Column {
Spacer(modifier = Modifier.height(12.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
post.tags.forEach { tag ->
SuggestionChip(
onClick = {},
label = {
Text("#$tag", style = MaterialTheme.typography.labelMedium)
},
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer
),
border = null
)
}
if (post.linkDescription != null) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = post.linkDescription,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// Section 3 — Image gallery
AnimatedVisibility(
visible = sectionVisible[3].value && allImages.isNotEmpty(),
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
) {
Column {
Spacer(modifier = Modifier.height(16.dp))
DetailImageGallery(
images = allImages,
onImageClick = { index ->
galleryStartIndex = index
showGallery = true
}
Spacer(modifier = Modifier.height(4.dp))
)
// Alt text display
if (!post.imageAlt.isNullOrBlank()) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = post.linkUrl,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
text = "Alt: ${post.imageAlt}",
style = MaterialTheme.typography.bodySmall.copy(
fontStyle = FontStyle.Italic
),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// Stats section
Spacer(modifier = Modifier.height(24.dp))
PostStatsSection(post)
// Section 4 — Link preview
AnimatedVisibility(
visible = sectionVisible[4].value && post.linkUrl != null,
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
) {
Column {
Spacer(modifier = Modifier.height(16.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(160.dp)
.clip(MaterialTheme.shapes.small),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(8.dp))
}
if (post.linkTitle != null) {
Text(
text = post.linkTitle,
style = MaterialTheme.typography.titleMedium
)
}
if (post.linkDescription != null) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = post.linkDescription,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(4.dp))
if (post.linkUrl != null) {
Text(
text = post.linkUrl,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
// Section 5 — PostStatsSection
AnimatedVisibility(
visible = sectionVisible[5].value,
enter = slideInVertically(initialOffsetY = { it / 4 }, animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick())
) {
Column {
Spacer(modifier = Modifier.height(24.dp))
PostStatsSection(post)
}
}
}
}
// D3: Animated delete dialog
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Delete Post") },
text = { Text("Are you sure you want to delete this post? This action cannot be undone.") },
confirmButton = {
TextButton(
onClick = {
showDeleteDialog = false
onDelete(post)
},
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) { Text("Delete") }
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") }
AnimatedDialog(onDismissRequest = { showDeleteDialog = false }) {
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
Text("Delete Post", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Text("Are you sure you want to delete this post? This action cannot be undone.")
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") }
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
showDeleteDialog = false
onDelete(post)
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) { Text("Delete") }
}
}
}
)
}
}
// Full-screen gallery