diff --git a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt index 36b8dd9..5b409cc 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt @@ -4,6 +4,8 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.* +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -40,6 +42,7 @@ import com.swoosh.microblog.data.HashtagParser import com.swoosh.microblog.data.model.FeedPost import com.swoosh.microblog.data.model.PostStats import com.swoosh.microblog.ui.animation.SwooshMotion +import kotlinx.coroutines.delay import com.swoosh.microblog.ui.components.PulsingPlaceholder import com.swoosh.microblog.ui.preview.HtmlPreviewWebView import java.time.LocalDateTime @@ -168,7 +171,12 @@ fun ComposerScreen( } } - if (state.isPreviewMode) { + Crossfade( + targetState = state.isPreviewMode, + animationSpec = SwooshMotion.quick(), + label = "editPreviewCrossfade" + ) { isPreview -> + if (isPreview) { // Preview mode: show rendered HTML if (state.text.isBlank() && state.imageUris.isEmpty() && state.linkPreview == null) { Box( @@ -212,52 +220,63 @@ fun ComposerScreen( supportingText = { val charCount = state.text.length val statsText = PostStats.formatComposerStats(state.text) - val color = when { + val targetColor = when { charCount > 500 -> MaterialTheme.colorScheme.error charCount > 280 -> MaterialTheme.colorScheme.tertiary else -> MaterialTheme.colorScheme.onSurfaceVariant } + val animatedColor by animateColorAsState( + targetValue = targetColor, + animationSpec = SwooshMotion.quick(), + label = "counterColor" + ) Text( text = statsText, style = MaterialTheme.typography.labelSmall, - color = color + color = animatedColor ) } ) // Extracted tags preview chips - if (state.extractedTags.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - Icon( - Icons.Default.Tag, - contentDescription = "Tags", - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - @OptIn(ExperimentalLayoutApi::class) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + AnimatedVisibility( + visible = state.extractedTags.isNotEmpty(), + enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()), + exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy()) + ) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - state.extractedTags.forEach { tag -> - SuggestionChip( - onClick = {}, - label = { - Text( - "#$tag", - style = MaterialTheme.typography.labelSmall - ) - }, - colors = SuggestionChipDefaults.suggestionChipColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - labelColor = MaterialTheme.colorScheme.onPrimaryContainer - ), - border = null - ) + Icon( + Icons.Default.Tag, + contentDescription = "Tags", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + @OptIn(ExperimentalLayoutApi::class) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + state.extractedTags.forEach { tag -> + SuggestionChip( + onClick = {}, + label = { + Text( + "#$tag", + style = MaterialTheme.typography.labelSmall + ) + }, + colors = SuggestionChipDefaults.suggestionChipColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + labelColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + border = null + ) + } } } } @@ -452,43 +471,63 @@ fun ComposerScreen( // Action buttons Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Button( - onClick = viewModel::publish, - modifier = Modifier.fillMaxWidth(), - enabled = !state.isSubmitting && (state.text.isNotBlank() || state.imageUris.isNotEmpty()) + // Publish button (step 1) + var publishVisible by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { publishVisible = true } + AnimatedVisibility( + visible = publishVisible, + enter = scaleIn(animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick()) ) { - if (state.isSubmitting) { - CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp) - Spacer(Modifier.width(8.dp)) + Button( + onClick = viewModel::publish, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isSubmitting && (state.text.isNotBlank() || state.imageUris.isNotEmpty()) + ) { + if (state.isSubmitting) { + CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp) + Spacer(Modifier.width(8.dp)) + } + Text(if (state.isEditing) "Update & Publish" else "Publish now") } - Text(if (state.isEditing) "Update & Publish" else "Publish now") } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + // Draft + Schedule row (step 2) + var rowVisible by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(SwooshMotion.StaggerDelayMs) + rowVisible = true + } + AnimatedVisibility( + visible = rowVisible, + enter = scaleIn(animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick()) ) { - OutlinedButton( - onClick = viewModel::saveDraft, - modifier = Modifier.weight(1f), - enabled = !state.isSubmitting + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text("Save draft") - } + OutlinedButton( + onClick = viewModel::saveDraft, + modifier = Modifier.weight(1f), + enabled = !state.isSubmitting + ) { + Text("Save draft") + } - OutlinedButton( - onClick = { showDatePicker = true }, - modifier = Modifier.weight(1f), - enabled = !state.isSubmitting - ) { - Icon(Icons.Default.Schedule, null, Modifier.size(18.dp)) - Spacer(Modifier.width(4.dp)) - Text("Schedule") + OutlinedButton( + onClick = { showDatePicker = true }, + modifier = Modifier.weight(1f), + enabled = !state.isSubmitting + ) { + Icon(Icons.Default.Schedule, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Schedule") + } } } } } } + } // end Crossfade } }