mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
refactor: extract ConfirmationDialog, fix animation efficiency issues
This commit is contained in:
parent
15c678556e
commit
2470f9a049
4 changed files with 132 additions and 132 deletions
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ import com.swoosh.microblog.data.model.LinkPreview
|
||||||
import com.swoosh.microblog.data.model.PostStats
|
import com.swoosh.microblog.data.model.PostStats
|
||||||
import com.swoosh.microblog.data.model.QueueStatus
|
import com.swoosh.microblog.data.model.QueueStatus
|
||||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
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.FullScreenGallery
|
||||||
import com.swoosh.microblog.ui.feed.StatusBadge
|
import com.swoosh.microblog.ui.feed.StatusBadge
|
||||||
import com.swoosh.microblog.ui.feed.formatRelativeTime
|
import com.swoosh.microblog.ui.feed.formatRelativeTime
|
||||||
|
|
@ -363,30 +363,16 @@ fun DetailScreen(
|
||||||
|
|
||||||
// D3: Animated delete dialog
|
// D3: Animated delete dialog
|
||||||
if (showDeleteDialog) {
|
if (showDeleteDialog) {
|
||||||
AnimatedDialog(onDismissRequest = { showDeleteDialog = false }) {
|
ConfirmationDialog(
|
||||||
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
|
title = "Delete Post",
|
||||||
Column(modifier = Modifier.padding(24.dp)) {
|
message = "Are you sure you want to delete this post? This action cannot be undone.",
|
||||||
Text("Delete Post", style = MaterialTheme.typography.headlineSmall)
|
confirmLabel = "Delete",
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
onConfirm = {
|
||||||
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 = {
|
|
||||||
showDeleteDialog = false
|
showDeleteDialog = false
|
||||||
onDelete(post)
|
onDelete(post)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
|
onDismiss = { showDeleteDialog = false }
|
||||||
) { Text("Delete") }
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-screen gallery
|
// Full-screen gallery
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ import com.swoosh.microblog.data.model.PostFilter
|
||||||
import com.swoosh.microblog.data.model.PostStats
|
import com.swoosh.microblog.data.model.PostStats
|
||||||
import com.swoosh.microblog.data.model.QueueStatus
|
import com.swoosh.microblog.data.model.QueueStatus
|
||||||
import com.swoosh.microblog.data.model.SortOrder
|
import com.swoosh.microblog.data.model.SortOrder
|
||||||
|
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -661,6 +662,7 @@ fun FeedScreen(
|
||||||
if (state.posts.isNotEmpty() && !initialLoadComplete) {
|
if (state.posts.isNotEmpty() && !initialLoadComplete) {
|
||||||
delay(SwooshMotion.StaggerDelayMs * minOf(state.posts.size, 8) + 300)
|
delay(SwooshMotion.StaggerDelayMs * minOf(state.posts.size, 8) + 300)
|
||||||
initialLoadComplete = true
|
initialLoadComplete = true
|
||||||
|
animatedKeys.clear() // Free memory — no longer needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,25 +711,16 @@ fun FeedScreen(
|
||||||
|
|
||||||
// Delete confirmation dialog
|
// Delete confirmation dialog
|
||||||
if (postPendingDelete != null) {
|
if (postPendingDelete != null) {
|
||||||
AlertDialog(
|
ConfirmationDialog(
|
||||||
onDismissRequest = { postPendingDelete = null },
|
title = "Delete this post?",
|
||||||
title = { Text("Delete this post?") },
|
message = "This action cannot be undone.",
|
||||||
text = { Text("This action cannot be undone.") },
|
confirmLabel = "Delete",
|
||||||
confirmButton = {
|
onConfirm = {
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
val post = postPendingDelete!!
|
val post = postPendingDelete!!
|
||||||
postPendingDelete = null
|
postPendingDelete = null
|
||||||
viewModel.deletePostWithUndo(post)
|
viewModel.deletePostWithUndo(post)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.textButtonColors(
|
onDismiss = { postPendingDelete = null }
|
||||||
contentColor = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
) { Text("Delete") }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { postPendingDelete = null }) { Text("Cancel") }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,27 +749,16 @@ fun FeedScreen(
|
||||||
|
|
||||||
// Account delete confirmation dialog
|
// Account delete confirmation dialog
|
||||||
showDeleteConfirmation?.let { account ->
|
showDeleteConfirmation?.let { account ->
|
||||||
AlertDialog(
|
ConfirmationDialog(
|
||||||
onDismissRequest = { showDeleteConfirmation = null },
|
title = "Remove Account",
|
||||||
title = { Text("Remove Account") },
|
message = "Remove \"${account.name}\"? Local drafts for this account will be kept.",
|
||||||
text = { Text("Remove \"${account.name}\"? Local drafts for this account will be kept.") },
|
confirmLabel = "Remove",
|
||||||
confirmButton = {
|
onConfirm = {
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.removeAccount(account.id)
|
viewModel.removeAccount(account.id)
|
||||||
showDeleteConfirmation = null
|
showDeleteConfirmation = null
|
||||||
if (accounts.size <= 1) {
|
if (accounts.size <= 1) showAccountSwitcher = false
|
||||||
showAccountSwitcher = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.textButtonColors(
|
onDismiss = { showDeleteConfirmation = null }
|
||||||
contentColor = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
) { Text("Remove") }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showDeleteConfirmation = null }) { Text("Cancel") }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1127,7 +1109,7 @@ fun AccountSwitcherBottomSheet(
|
||||||
|
|
||||||
accounts.forEachIndexed { index, account ->
|
accounts.forEachIndexed { index, account ->
|
||||||
var itemVisible by remember { mutableStateOf(false) }
|
var itemVisible by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(account.id) {
|
||||||
delay(SwooshMotion.StaggerDelayMs * index)
|
delay(SwooshMotion.StaggerDelayMs * index)
|
||||||
itemVisible = true
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PostCardContent(
|
fun PostCardContent(
|
||||||
|
|
@ -1617,21 +1630,10 @@ fun PostCardContent(
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val isUploading = post.queueStatus == QueueStatus.UPLOADING
|
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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
AssistChip(
|
PulsingAssistChip(
|
||||||
onClick = {},
|
label = queueLabel,
|
||||||
label = { Text(queueLabel, style = MaterialTheme.typography.labelSmall) },
|
isUploading = isUploading
|
||||||
modifier = Modifier.graphicsLayer { alpha = if (isUploading) chipAlpha else 1f }
|
|
||||||
)
|
)
|
||||||
if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) {
|
if (post.queueStatus == QueueStatus.QUEUED_PUBLISH || post.queueStatus == QueueStatus.QUEUED_SCHEDULED) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.swoosh.microblog.data.AccountManager
|
import com.swoosh.microblog.data.AccountManager
|
||||||
import com.swoosh.microblog.data.api.ApiClient
|
import com.swoosh.microblog.data.api.ApiClient
|
||||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
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.feed.AccountAvatar
|
||||||
import com.swoosh.microblog.ui.theme.ThemeMode
|
import com.swoosh.microblog.ui.theme.ThemeMode
|
||||||
import com.swoosh.microblog.ui.theme.ThemeViewModel
|
import com.swoosh.microblog.ui.theme.ThemeViewModel
|
||||||
|
|
@ -177,21 +177,11 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDisconnectDialog) {
|
if (showDisconnectDialog) {
|
||||||
AnimatedDialog(onDismissRequest = { showDisconnectDialog = false }) {
|
ConfirmationDialog(
|
||||||
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
|
title = "Disconnect Account?",
|
||||||
Column(modifier = Modifier.padding(24.dp)) {
|
message = "Remove \"${activeAccount?.name ?: ""}\"? You'll need to set up again.",
|
||||||
Text("Disconnect Account?", style = MaterialTheme.typography.headlineSmall)
|
confirmLabel = "Disconnect",
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
onConfirm = {
|
||||||
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 = {
|
|
||||||
showDisconnectDialog = false
|
showDisconnectDialog = false
|
||||||
activeAccount?.let { account ->
|
activeAccount?.let { account ->
|
||||||
accountManager.removeAccount(account.id)
|
accountManager.removeAccount(account.id)
|
||||||
|
|
@ -199,41 +189,23 @@ fun SettingsScreen(
|
||||||
if (accountManager.getAccounts().isEmpty()) onLogout() else onBack()
|
if (accountManager.getAccounts().isEmpty()) onLogout() else onBack()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
|
onDismiss = { showDisconnectDialog = false }
|
||||||
) { Text("Disconnect") }
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDisconnectAllDialog) {
|
if (showDisconnectAllDialog) {
|
||||||
AnimatedDialog(onDismissRequest = { showDisconnectAllDialog = false }) {
|
ConfirmationDialog(
|
||||||
Card(modifier = Modifier.padding(horizontal = 24.dp)) {
|
title = "Disconnect All?",
|
||||||
Column(modifier = Modifier.padding(24.dp)) {
|
message = "Remove all accounts? You'll need to set up from scratch.",
|
||||||
Text("Disconnect All?", style = MaterialTheme.typography.headlineSmall)
|
confirmLabel = "Disconnect All",
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
onConfirm = {
|
||||||
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 = {
|
|
||||||
showDisconnectAllDialog = false
|
showDisconnectAllDialog = false
|
||||||
accountManager.clearAll()
|
accountManager.clearAll()
|
||||||
ApiClient.reset()
|
ApiClient.reset()
|
||||||
onLogout()
|
onLogout()
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
|
onDismiss = { showDisconnectAllDialog = false }
|
||||||
) { Text("Disconnect All") }
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue