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 012485b..36b8dd9 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 @@ -3,6 +3,8 @@ package com.swoosh.microblog.ui.composer import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells @@ -37,6 +39,8 @@ import coil.compose.AsyncImage 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 com.swoosh.microblog.ui.components.PulsingPlaceholder import com.swoosh.microblog.ui.preview.HtmlPreviewWebView import java.time.LocalDateTime import java.time.ZoneId @@ -274,107 +278,131 @@ fun ComposerScreen( } // Image grid preview (multi-image) - if (state.imageUris.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - ImageGridPreview( - imageUris = state.imageUris, - onRemoveImage = viewModel::removeImage, - onAddMore = { multiImagePickerLauncher.launch("image/*") } - ) + AnimatedVisibility( + visible = state.imageUris.isNotEmpty(), + enter = scaleIn(initialScale = 0f, animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()), + exit = scaleOut(animationSpec = SwooshMotion.quick()) + fadeOut(SwooshMotion.quick()) + ) { + Column { + Spacer(modifier = Modifier.height(12.dp)) + ImageGridPreview( + imageUris = state.imageUris, + onRemoveImage = viewModel::removeImage, + onAddMore = { multiImagePickerLauncher.launch("image/*") } + ) - // Alt text for the first/primary image - Spacer(modifier = Modifier.height(4.dp)) - TextButton( - onClick = { showAltTextDialog = true }, - contentPadding = PaddingValues(horizontal = 0.dp) - ) { - Text( - text = if (state.imageAlt.isBlank()) "Add alt text" else "Edit alt text", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary - ) - } - // ALT badge when alt text is set - if (state.imageAlt.isNotBlank()) { - Text( - text = "ALT", - modifier = Modifier - .background( - MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.8f), - RoundedCornerShape(4.dp) - ) - .padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.inverseOnSurface, - fontSize = 11.sp, - fontWeight = FontWeight.Bold - ) + // Alt text for the first/primary image + Spacer(modifier = Modifier.height(4.dp)) + TextButton( + onClick = { showAltTextDialog = true }, + contentPadding = PaddingValues(horizontal = 0.dp) + ) { + Text( + text = if (state.imageAlt.isBlank()) "Add alt text" else "Edit alt text", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + } + // ALT badge when alt text is set + if (state.imageAlt.isNotBlank()) { + Text( + text = "ALT", + modifier = Modifier + .background( + MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.8f), + RoundedCornerShape(4.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp), + color = MaterialTheme.colorScheme.inverseOnSurface, + fontSize = 11.sp, + fontWeight = FontWeight.Bold + ) + } } } // Link preview - if (state.isLoadingLink) { - Spacer(modifier = Modifier.height(12.dp)) - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + AnimatedVisibility( + visible = state.isLoadingLink, + enter = fadeIn(SwooshMotion.quick()), + exit = fadeOut(SwooshMotion.quick()) + ) { + Column { + Spacer(modifier = Modifier.height(12.dp)) + PulsingPlaceholder(height = 80.dp) + } } - if (state.linkPreview != null) { - Spacer(modifier = Modifier.height(12.dp)) - OutlinedCard(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(12.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = state.linkPreview!!.title ?: state.linkPreview!!.url, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.weight(1f) - ) - IconButton(onClick = viewModel::removeLinkPreview) { - Icon(Icons.Default.Close, "Remove link", Modifier.size(18.dp)) + AnimatedVisibility( + visible = state.linkPreview != null && !state.isLoadingLink, + enter = slideInVertically(initialOffsetY = { it / 2 }, animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick()), + exit = fadeOut(SwooshMotion.quick()) + ) { + Column { + Spacer(modifier = Modifier.height(12.dp)) + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = state.linkPreview!!.title ?: state.linkPreview!!.url, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = viewModel::removeLinkPreview) { + Icon(Icons.Default.Close, "Remove link", Modifier.size(18.dp)) + } + } + if (state.linkPreview!!.description != null) { + Text( + text = state.linkPreview!!.description!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2 + ) + } + if (state.linkPreview!!.imageUrl != null) { + Spacer(modifier = Modifier.height(8.dp)) + AsyncImage( + model = state.linkPreview!!.imageUrl, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .clip(MaterialTheme.shapes.small), + contentScale = ContentScale.Crop + ) } - } - if (state.linkPreview!!.description != null) { - Text( - text = state.linkPreview!!.description!!, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2 - ) - } - if (state.linkPreview!!.imageUrl != null) { - Spacer(modifier = Modifier.height(8.dp)) - AsyncImage( - model = state.linkPreview!!.imageUrl, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .height(120.dp) - .clip(MaterialTheme.shapes.small), - contentScale = ContentScale.Crop - ) } } } } // Scheduled time display - if (state.scheduledAt != null) { - Spacer(modifier = Modifier.height(12.dp)) - AssistChip( - onClick = { showDatePicker = true }, - label = { Text("Scheduled: ${state.scheduledAt}") }, - leadingIcon = { Icon(Icons.Default.Schedule, null, Modifier.size(18.dp)) }, - trailingIcon = { - IconButton( - onClick = { viewModel.setScheduledDate(null) }, - modifier = Modifier.size(18.dp) - ) { - Icon(Icons.Default.Close, "Clear schedule", Modifier.size(14.dp)) + AnimatedVisibility( + visible = state.scheduledAt != null, + enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()), + exit = scaleOut(animationSpec = SwooshMotion.quick()) + fadeOut(SwooshMotion.quick()) + ) { + Column { + Spacer(modifier = Modifier.height(12.dp)) + AssistChip( + onClick = { showDatePicker = true }, + label = { Text("Scheduled: ${state.scheduledAt}") }, + leadingIcon = { Icon(Icons.Default.Schedule, null, Modifier.size(18.dp)) }, + trailingIcon = { + IconButton( + onClick = { viewModel.setScheduledDate(null) }, + modifier = Modifier.size(18.dp) + ) { + Icon(Icons.Default.Close, "Clear schedule", Modifier.size(14.dp)) + } } - } - ) + ) + } } // Feature toggle @@ -404,13 +432,19 @@ fun ComposerScreen( ) } - if (state.error != null) { - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = state.error!!, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall - ) + AnimatedVisibility( + visible = state.error != null, + enter = slideInHorizontally(initialOffsetX = { -it / 4 }, animationSpec = SwooshMotion.snappy()) + fadeIn(SwooshMotion.quick()), + exit = fadeOut(SwooshMotion.quick()) + ) { + Column { + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = state.error!!, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } } Spacer(modifier = Modifier.height(24.dp))