mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +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 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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue