mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: add counter, buttons, hashtag, and preview animations in composer
This commit is contained in:
parent
4a7005ce1e
commit
15c678556e
1 changed files with 98 additions and 59 deletions
|
|
@ -4,6 +4,8 @@ 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.*
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.*
|
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.*
|
||||||
|
|
@ -40,6 +42,7 @@ 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.animation.SwooshMotion
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import com.swoosh.microblog.ui.components.PulsingPlaceholder
|
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
|
||||||
|
|
@ -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
|
// Preview mode: show rendered HTML
|
||||||
if (state.text.isBlank() && state.imageUris.isEmpty() && state.linkPreview == null) {
|
if (state.text.isBlank() && state.imageUris.isEmpty() && state.linkPreview == null) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -212,52 +220,63 @@ fun ComposerScreen(
|
||||||
supportingText = {
|
supportingText = {
|
||||||
val charCount = state.text.length
|
val charCount = state.text.length
|
||||||
val statsText = PostStats.formatComposerStats(state.text)
|
val statsText = PostStats.formatComposerStats(state.text)
|
||||||
val color = when {
|
val targetColor = when {
|
||||||
charCount > 500 -> MaterialTheme.colorScheme.error
|
charCount > 500 -> MaterialTheme.colorScheme.error
|
||||||
charCount > 280 -> MaterialTheme.colorScheme.tertiary
|
charCount > 280 -> MaterialTheme.colorScheme.tertiary
|
||||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
}
|
}
|
||||||
|
val animatedColor by animateColorAsState(
|
||||||
|
targetValue = targetColor,
|
||||||
|
animationSpec = SwooshMotion.quick(),
|
||||||
|
label = "counterColor"
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = statsText,
|
text = statsText,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = color
|
color = animatedColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Extracted tags preview chips
|
// Extracted tags preview chips
|
||||||
if (state.extractedTags.isNotEmpty()) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
visible = state.extractedTags.isNotEmpty(),
|
||||||
Row(
|
enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
) {
|
||||||
) {
|
Column {
|
||||||
Icon(
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Icons.Default.Tag,
|
Row(
|
||||||
contentDescription = "Tags",
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.size(18.dp),
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
) {
|
||||||
state.extractedTags.forEach { tag ->
|
Icon(
|
||||||
SuggestionChip(
|
Icons.Default.Tag,
|
||||||
onClick = {},
|
contentDescription = "Tags",
|
||||||
label = {
|
modifier = Modifier.size(18.dp),
|
||||||
Text(
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
"#$tag",
|
)
|
||||||
style = MaterialTheme.typography.labelSmall
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
)
|
FlowRow(
|
||||||
},
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
colors = SuggestionChipDefaults.suggestionChipColors(
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
) {
|
||||||
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
|
state.extractedTags.forEach { tag ->
|
||||||
),
|
SuggestionChip(
|
||||||
border = null
|
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
|
// Action buttons
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Button(
|
// Publish button (step 1)
|
||||||
onClick = viewModel::publish,
|
var publishVisible by remember { mutableStateOf(false) }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
LaunchedEffect(Unit) { publishVisible = true }
|
||||||
enabled = !state.isSubmitting && (state.text.isNotBlank() || state.imageUris.isNotEmpty())
|
AnimatedVisibility(
|
||||||
|
visible = publishVisible,
|
||||||
|
enter = scaleIn(animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick())
|
||||||
) {
|
) {
|
||||||
if (state.isSubmitting) {
|
Button(
|
||||||
CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
onClick = viewModel::publish,
|
||||||
Spacer(Modifier.width(8.dp))
|
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(
|
// Draft + Schedule row (step 2)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
var rowVisible by remember { mutableStateOf(false) }
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
LaunchedEffect(Unit) {
|
||||||
|
delay(SwooshMotion.StaggerDelayMs)
|
||||||
|
rowVisible = true
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = rowVisible,
|
||||||
|
enter = scaleIn(animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick())
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
Row(
|
||||||
onClick = viewModel::saveDraft,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.weight(1f),
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
enabled = !state.isSubmitting
|
|
||||||
) {
|
) {
|
||||||
Text("Save draft")
|
OutlinedButton(
|
||||||
}
|
onClick = viewModel::saveDraft,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = !state.isSubmitting
|
||||||
|
) {
|
||||||
|
Text("Save draft")
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { showDatePicker = true },
|
onClick = { showDatePicker = true },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
enabled = !state.isSubmitting
|
enabled = !state.isSubmitting
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Schedule, null, Modifier.size(18.dp))
|
Icon(Icons.Default.Schedule, null, Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Schedule")
|
Text("Schedule")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // end Crossfade
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue