mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: add content reveal and animated delete dialog in detail
This commit is contained in:
parent
5183862533
commit
4a7005ce1e
1 changed files with 170 additions and 112 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue