mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: add image, link, schedule, and error animations in composer
This commit is contained in:
parent
0713bd912e
commit
188c62f076
1 changed files with 127 additions and 93 deletions
|
|
@ -3,6 +3,8 @@ package com.swoosh.microblog.ui.composer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
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.HashtagParser
|
||||||
import com.swoosh.microblog.data.model.FeedPost
|
import com.swoosh.microblog.data.model.FeedPost
|
||||||
import com.swoosh.microblog.data.model.PostStats
|
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 com.swoosh.microblog.ui.preview.HtmlPreviewWebView
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
@ -274,107 +278,131 @@ fun ComposerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image grid preview (multi-image)
|
// Image grid preview (multi-image)
|
||||||
if (state.imageUris.isNotEmpty()) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
visible = state.imageUris.isNotEmpty(),
|
||||||
ImageGridPreview(
|
enter = scaleIn(initialScale = 0f, animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()),
|
||||||
imageUris = state.imageUris,
|
exit = scaleOut(animationSpec = SwooshMotion.quick()) + fadeOut(SwooshMotion.quick())
|
||||||
onRemoveImage = viewModel::removeImage,
|
) {
|
||||||
onAddMore = { multiImagePickerLauncher.launch("image/*") }
|
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
|
// Alt text for the first/primary image
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { showAltTextDialog = true },
|
onClick = { showAltTextDialog = true },
|
||||||
contentPadding = PaddingValues(horizontal = 0.dp)
|
contentPadding = PaddingValues(horizontal = 0.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (state.imageAlt.isBlank()) "Add alt text" else "Edit alt text",
|
text = if (state.imageAlt.isBlank()) "Add alt text" else "Edit alt text",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// ALT badge when alt text is set
|
// ALT badge when alt text is set
|
||||||
if (state.imageAlt.isNotBlank()) {
|
if (state.imageAlt.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = "ALT",
|
text = "ALT",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.8f),
|
MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.8f),
|
||||||
RoundedCornerShape(4.dp)
|
RoundedCornerShape(4.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
color = MaterialTheme.colorScheme.inverseOnSurface,
|
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link preview
|
// Link preview
|
||||||
if (state.isLoadingLink) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
visible = state.isLoadingLink,
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
enter = fadeIn(SwooshMotion.quick()),
|
||||||
|
exit = fadeOut(SwooshMotion.quick())
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
PulsingPlaceholder(height = 80.dp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.linkPreview != null) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
visible = state.linkPreview != null && !state.isLoadingLink,
|
||||||
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
enter = slideInVertically(initialOffsetY = { it / 2 }, animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick()),
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
exit = fadeOut(SwooshMotion.quick())
|
||||||
Row(
|
) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Column {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
verticalAlignment = Alignment.CenterVertically
|
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
Text(
|
Row(
|
||||||
text = state.linkPreview!!.title ?: state.linkPreview!!.url,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.weight(1f)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
)
|
) {
|
||||||
IconButton(onClick = viewModel::removeLinkPreview) {
|
Text(
|
||||||
Icon(Icons.Default.Close, "Remove link", Modifier.size(18.dp))
|
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
|
// Scheduled time display
|
||||||
if (state.scheduledAt != null) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
visible = state.scheduledAt != null,
|
||||||
AssistChip(
|
enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()),
|
||||||
onClick = { showDatePicker = true },
|
exit = scaleOut(animationSpec = SwooshMotion.quick()) + fadeOut(SwooshMotion.quick())
|
||||||
label = { Text("Scheduled: ${state.scheduledAt}") },
|
) {
|
||||||
leadingIcon = { Icon(Icons.Default.Schedule, null, Modifier.size(18.dp)) },
|
Column {
|
||||||
trailingIcon = {
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
IconButton(
|
AssistChip(
|
||||||
onClick = { viewModel.setScheduledDate(null) },
|
onClick = { showDatePicker = true },
|
||||||
modifier = Modifier.size(18.dp)
|
label = { Text("Scheduled: ${state.scheduledAt}") },
|
||||||
) {
|
leadingIcon = { Icon(Icons.Default.Schedule, null, Modifier.size(18.dp)) },
|
||||||
Icon(Icons.Default.Close, "Clear schedule", Modifier.size(14.dp))
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.setScheduledDate(null) },
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, "Clear schedule", Modifier.size(14.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feature toggle
|
// Feature toggle
|
||||||
|
|
@ -404,13 +432,19 @@ fun ComposerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.error != null) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
visible = state.error != null,
|
||||||
Text(
|
enter = slideInHorizontally(initialOffsetX = { -it / 4 }, animationSpec = SwooshMotion.snappy()) + fadeIn(SwooshMotion.quick()),
|
||||||
text = state.error!!,
|
exit = fadeOut(SwooshMotion.quick())
|
||||||
color = MaterialTheme.colorScheme.error,
|
) {
|
||||||
style = MaterialTheme.typography.bodySmall
|
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))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue