refactor: extract ConfirmationDialog, fix animation efficiency issues

This commit is contained in:
Paweł Orzech 2026-03-19 14:32:24 +01:00
parent 15c678556e
commit 2470f9a049
No known key found for this signature in database
4 changed files with 132 additions and 132 deletions

View file

@ -0,0 +1,40 @@
package com.swoosh.microblog.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ConfirmationDialog(
title: String,
message: String,
confirmLabel: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AnimatedDialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
Text(title, style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Text(message)
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismiss) { Text("Cancel") }
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = onConfirm,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) { Text(confirmLabel) }
}
}
}
}
}

View file

@ -53,7 +53,7 @@ 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.components.ConfirmationDialog
import com.swoosh.microblog.ui.feed.FullScreenGallery
import com.swoosh.microblog.ui.feed.StatusBadge
import com.swoosh.microblog.ui.feed.formatRelativeTime
@ -363,30 +363,16 @@ fun DetailScreen(
// D3: Animated delete dialog
if (showDeleteDialog) {
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 = {
ConfirmationDialog(
title = "Delete Post",
message = "Are you sure you want to delete this post? This action cannot be undone.",
confirmLabel = "Delete",
onConfirm = {
showDeleteDialog = false
onDelete(post)
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) { Text("Delete") }
}
}
}
}
onDismiss = { showDeleteDialog = false }
)
}
// Full-screen gallery

View file

@ -88,6 +88,7 @@ import com.swoosh.microblog.data.model.PostFilter
import com.swoosh.microblog.data.model.PostStats
import com.swoosh.microblog.data.model.QueueStatus
import com.swoosh.microblog.data.model.SortOrder
import com.swoosh.microblog.ui.components.ConfirmationDialog
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
@ -661,6 +662,7 @@ fun FeedScreen(
if (state.posts.isNotEmpty() && !initialLoadComplete) {
delay(SwooshMotion.StaggerDelayMs * minOf(state.posts.size, 8) + 300)
initialLoadComplete = true
animatedKeys.clear() // Free memory — no longer needed
}
}
@ -709,25 +711,16 @@ fun FeedScreen(
// Delete confirmation dialog
if (postPendingDelete != null) {
AlertDialog(
onDismissRequest = { postPendingDelete = null },
title = { Text("Delete this post?") },
text = { Text("This action cannot be undone.") },
confirmButton = {
TextButton(
onClick = {
ConfirmationDialog(
title = "Delete this post?",
message = "This action cannot be undone.",
confirmLabel = "Delete",
onConfirm = {
val post = postPendingDelete!!
postPendingDelete = null
viewModel.deletePostWithUndo(post)
},
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) { Text("Delete") }
},
dismissButton = {
TextButton(onClick = { postPendingDelete = null }) { Text("Cancel") }
}
onDismiss = { postPendingDelete = null }
)
}
@ -756,27 +749,16 @@ fun FeedScreen(
// Account delete confirmation dialog
showDeleteConfirmation?.let { account ->
AlertDialog(
onDismissRequest = { showDeleteConfirmation = null },
title = { Text("Remove Account") },
text = { Text("Remove \"${account.name}\"? Local drafts for this account will be kept.") },
confirmButton = {
TextButton(
onClick = {
ConfirmationDialog(
title = "Remove Account",
message = "Remove \"${account.name}\"? Local drafts for this account will be kept.",
confirmLabel = "Remove",
onConfirm = {
viewModel.removeAccount(account.id)
showDeleteConfirmation = null
if (accounts.size <= 1) {
showAccountSwitcher = false
}
if (accounts.size <= 1) showAccountSwitcher = false
},
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) { Text("Remove") }
},
dismissButton = {
TextButton(onClick = { showDeleteConfirmation = null }) { Text("Cancel") }
}
onDismiss = { showDeleteConfirmation = null }
)
}
@ -1127,7 +1109,7 @@ fun AccountSwitcherBottomSheet(
accounts.forEachIndexed { index, account ->
var itemVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
LaunchedEffect(account.id) {
delay(SwooshMotion.StaggerDelayMs * index)
itemVisible = true
}
@ -1395,6 +1377,37 @@ fun RecentSearchesList(
}
}
@Composable
private fun PulsingAssistChip(
label: String,
isUploading: Boolean,
modifier: Modifier = Modifier
) {
if (isUploading) {
val infiniteTransition = rememberInfiniteTransition(label = "queuePulse")
val chipAlpha by infiniteTransition.animateFloat(
initialValue = 0.6f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600),
repeatMode = RepeatMode.Reverse
),
label = "uploadPulse"
)
AssistChip(
onClick = {},
label = { Text(label, style = MaterialTheme.typography.labelSmall) },
modifier = modifier.graphicsLayer { alpha = chipAlpha }
)
} else {
AssistChip(
onClick = {},
label = { Text(label, style = MaterialTheme.typography.labelSmall) },
modifier = modifier
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostCardContent(
@ -1617,21 +1630,10 @@ fun PostCardContent(
else -> ""
}
val isUploading = post.queueStatus == QueueStatus.UPLOADING
val infiniteTransition = rememberInfiniteTransition(label = "queuePulse")
val chipAlpha by infiniteTransition.animateFloat(
initialValue = if (isUploading) 0.6f else 1f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600),
repeatMode = RepeatMode.Reverse
),
label = "uploadPulse"
)
Row(verticalAlignment = Alignment.CenterVertically) {
AssistChip(
onClick = {},
label = { Text(queueLabel, style = MaterialTheme.typography.labelSmall) },
modifier = Modifier.graphicsLayer { alpha = if (isUploading) chipAlpha else 1f }
PulsingAssistChip(
label = queueLabel,
isUploading = isUploading
)
if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) {
Spacer(modifier = Modifier.width(8.dp))

View file

@ -21,7 +21,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.swoosh.microblog.data.AccountManager
import com.swoosh.microblog.data.api.ApiClient
import com.swoosh.microblog.ui.animation.SwooshMotion
import com.swoosh.microblog.ui.components.AnimatedDialog
import com.swoosh.microblog.ui.components.ConfirmationDialog
import com.swoosh.microblog.ui.feed.AccountAvatar
import com.swoosh.microblog.ui.theme.ThemeMode
import com.swoosh.microblog.ui.theme.ThemeViewModel
@ -177,21 +177,11 @@ fun SettingsScreen(
}
if (showDisconnectDialog) {
AnimatedDialog(onDismissRequest = { showDisconnectDialog = false }) {
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
Text("Disconnect Account?", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Text("Remove \"${activeAccount?.name ?: ""}\"? You'll need to set up again.")
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDisconnectDialog = false }) { Text("Cancel") }
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
ConfirmationDialog(
title = "Disconnect Account?",
message = "Remove \"${activeAccount?.name ?: ""}\"? You'll need to set up again.",
confirmLabel = "Disconnect",
onConfirm = {
showDisconnectDialog = false
activeAccount?.let { account ->
accountManager.removeAccount(account.id)
@ -199,41 +189,23 @@ fun SettingsScreen(
if (accountManager.getAccounts().isEmpty()) onLogout() else onBack()
}
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) { Text("Disconnect") }
}
}
}
}
onDismiss = { showDisconnectDialog = false }
)
}
if (showDisconnectAllDialog) {
AnimatedDialog(onDismissRequest = { showDisconnectAllDialog = false }) {
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
Text("Disconnect All?", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Text("Remove all accounts? You'll need to set up from scratch.")
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDisconnectAllDialog = false }) { Text("Cancel") }
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
ConfirmationDialog(
title = "Disconnect All?",
message = "Remove all accounts? You'll need to set up from scratch.",
confirmLabel = "Disconnect All",
onConfirm = {
showDisconnectAllDialog = false
accountManager.clearAll()
ApiClient.reset()
onLogout()
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) { Text("Disconnect All") }
}
}
}
}
onDismiss = { showDisconnectAllDialog = false }
)
}
}