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 android.content.Intent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically 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.shrinkVertically
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState 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.LinkPreview
import com.swoosh.microblog.data.model.PostStats import com.swoosh.microblog.data.model.PostStats
import com.swoosh.microblog.data.model.QueueStatus 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.FullScreenGallery
import com.swoosh.microblog.ui.feed.StatusBadge import com.swoosh.microblog.ui.feed.StatusBadge
import com.swoosh.microblog.ui.feed.formatRelativeTime import com.swoosh.microblog.ui.feed.formatRelativeTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@ -85,6 +93,16 @@ fun DetailScreen(
emptyList() 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -195,7 +213,11 @@ fun DetailScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(16.dp) .padding(16.dp)
) { ) {
// Status and time // Section 0 — Status and time
AnimatedVisibility(
visible = sectionVisible[0].value,
enter = fadeIn(SwooshMotion.quick()) + scaleIn(initialScale = 0.8f, animationSpec = SwooshMotion.bouncy())
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@ -207,17 +229,27 @@ fun DetailScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Full text content // Section 1 — Full text content
AnimatedVisibility(
visible = sectionVisible[1].value,
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
) {
Text( Text(
text = post.textContent, text = post.textContent,
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
}
// Tags // Section 2 — Tags
if (post.tags.isNotEmpty()) { 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)) Spacer(modifier = Modifier.height(12.dp))
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
@ -238,9 +270,14 @@ fun DetailScreen(
} }
} }
} }
}
// Image gallery // Section 3 — Image gallery
if (allImages.isNotEmpty()) { AnimatedVisibility(
visible = sectionVisible[3].value && allImages.isNotEmpty(),
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { 20 }, animationSpec = SwooshMotion.gentle())
) {
Column {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
DetailImageGallery( DetailImageGallery(
images = allImages, images = allImages,
@ -261,9 +298,14 @@ fun DetailScreen(
) )
} }
} }
}
// Link preview // Section 4 — Link preview
if (post.linkUrl != null) { 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)) Spacer(modifier = Modifier.height(16.dp))
OutlinedCard(modifier = Modifier.fillMaxWidth()) { OutlinedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(12.dp)) {
@ -294,6 +336,7 @@ fun DetailScreen(
) )
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
if (post.linkUrl != null) {
Text( Text(
text = post.linkUrl, text = post.linkUrl,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
@ -302,33 +345,48 @@ fun DetailScreen(
} }
} }
} }
}
}
// Stats section // 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)) Spacer(modifier = Modifier.height(24.dp))
PostStatsSection(post) PostStatsSection(post)
} }
} }
}
}
// D3: Animated delete dialog
if (showDeleteDialog) { if (showDeleteDialog) {
AlertDialog( AnimatedDialog(onDismissRequest = { showDeleteDialog = false }) {
onDismissRequest = { showDeleteDialog = false }, Card(modifier = Modifier.padding(horizontal = 24.dp)) {
title = { Text("Delete Post") }, Column(modifier = Modifier.padding(24.dp)) {
text = { Text("Are you sure you want to delete this post? This action cannot be undone.") }, Text("Delete Post", style = MaterialTheme.typography.headlineSmall)
confirmButton = { Spacer(modifier = Modifier.height(16.dp))
TextButton( 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 = { onClick = {
showDeleteDialog = false showDeleteDialog = false
onDelete(post) onDelete(post)
}, },
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
contentColor = MaterialTheme.colorScheme.error
)
) { Text("Delete") } ) { Text("Delete") }
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") }
} }
) }
}
}
} }
// Full-screen gallery // Full-screen gallery