diff --git a/app/src/main/java/com/swoosh/microblog/ui/components/ConfirmationDialog.kt b/app/src/main/java/com/swoosh/microblog/ui/components/ConfirmationDialog.kt new file mode 100644 index 0000000..f8dd053 --- /dev/null +++ b/app/src/main/java/com/swoosh/microblog/ui/components/ConfirmationDialog.kt @@ -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) } + } + } + } + } +} diff --git a/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt index 529da36..8fb0070 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/detail/DetailScreen.kt @@ -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 = { - showDeleteDialog = false - onDelete(post) - }, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) - ) { Text("Delete") } - } - } - } - } + 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) + }, + onDismiss = { showDeleteDialog = false } + ) } // Full-screen gallery diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt index 2bb06d8..ae2ef3d 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt @@ -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 = { - val post = postPendingDelete!! - postPendingDelete = null - viewModel.deletePostWithUndo(post) - }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { Text("Delete") } + ConfirmationDialog( + title = "Delete this post?", + message = "This action cannot be undone.", + confirmLabel = "Delete", + onConfirm = { + val post = postPendingDelete!! + postPendingDelete = null + viewModel.deletePostWithUndo(post) }, - 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 = { - viewModel.removeAccount(account.id) - showDeleteConfirmation = null - if (accounts.size <= 1) { - showAccountSwitcher = false - } - }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { Text("Remove") } + 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 }, - 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)) diff --git a/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt index 844a140..18f75af 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt @@ -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,63 +177,35 @@ 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 = { - showDisconnectDialog = false - activeAccount?.let { account -> - accountManager.removeAccount(account.id) - ApiClient.reset() - if (accountManager.getAccounts().isEmpty()) onLogout() else onBack() - } - }, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) - ) { Text("Disconnect") } - } + 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) + ApiClient.reset() + if (accountManager.getAccounts().isEmpty()) onLogout() else onBack() } - } - } + }, + 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 = { - showDisconnectAllDialog = false - accountManager.clearAll() - ApiClient.reset() - onLogout() - }, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) - ) { Text("Disconnect All") } - } - } - } - } + 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() + }, + onDismiss = { showDisconnectAllDialog = false } + ) } }