diff --git a/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt index 7c2ca1b..529da36 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt @@ -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