From b3adc56adf95319e7fc433d3095c02ecd349e278 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 22:11:54 +0000 Subject: [PATCH] feat: replace dialog with swipe-to-reveal delete button for drafts Instead of showing a confirmation dialog, the swipe now stays open revealing a red background with a clickable "Request Deletion" button. Supports swiping from both left-to-right and right-to-left directions. The user taps the revealed button to confirm deletion. https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU --- .../amethyst/ui/components/SwipeToDelete.kt | 106 ++++++++++-------- .../screen/loggedIn/drafts/DraftListScreen.kt | 2 +- amethyst/src/main/res/values/strings.xml | 1 - 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt index e64d2534b..b2748732e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -30,7 +31,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -40,14 +40,11 @@ import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart import androidx.compose.material3.SwipeToDismissBoxValue.Settled import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,6 +53,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -98,56 +96,26 @@ fun SwipeToDeleteContainer( @Composable fun SwipeToDeleteWithConfirmation( modifier: Modifier = Modifier, - onStartToEnd: () -> Unit, + onDelete: () -> Unit, content: @Composable (RowScope.() -> Unit), ) { - var showConfirmDialog by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() val dismissState = rememberSwipeToDismissBoxState( - confirmValueChange = { - when (it) { - StartToEnd -> { - showConfirmDialog = true - } - EndToStart -> {} - Settled -> {} - } - return@rememberSwipeToDismissBoxState false - }, positionalThreshold = { it * .40f }, ) - if (showConfirmDialog) { - AlertDialog( - onDismissRequest = { showConfirmDialog = false }, - title = { Text(text = stringRes(id = R.string.request_deletion)) }, - text = { Text(text = stringRes(id = R.string.delete_draft_confirmation)) }, - confirmButton = { - TextButton( - onClick = { - showConfirmDialog = false - onStartToEnd() - }, - ) { - Text(text = stringRes(id = R.string.yes)) - } - }, - dismissButton = { - TextButton( - onClick = { showConfirmDialog = false }, - ) { - Text(text = stringRes(id = R.string.no)) - } - }, - ) - } - SwipeToDismissBox( state = dismissState, modifier = modifier, - backgroundContent = { DismissBackground(dismissState) }, - enableDismissFromEndToStart = false, + backgroundContent = { + ConfirmDeleteBackground(dismissState) { + onDelete() + scope.launch { dismissState.reset() } + } + }, + enableDismissFromEndToStart = true, content = content, ) } @@ -191,3 +159,53 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState) { ) } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfirmDeleteBackground( + dismissState: SwipeToDismissBoxState, + onConfirmDelete: () -> Unit, +) { + val settled = dismissState.currentValue == Settled && dismissState.targetValue == Settled + + val color by animateColorAsState( + if (!settled) { + Color(0xFFFF1744) + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + label = "ConfirmDeleteBackground", + ) + + val haptic = LocalHapticFeedback.current + LaunchedEffect(key1 = dismissState.currentValue > dismissState.targetValue) { + if (dismissState.progress > 0 && dismissState.progress < 1) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + } + + Row( + modifier = + Modifier + .fillMaxSize() + .background(color) + .clickable(enabled = !settled) { onConfirmDelete() } + .padding(20.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + ) + Text( + text = stringRes(id = R.string.request_deletion), + color = Color.White, + style = MaterialTheme.typography.titleMedium, + ) + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index fcc2f3eb6..716b4545a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -170,7 +170,7 @@ private fun DraftFeedLoaded( Row(Modifier.fillMaxWidth().animateItem()) { SwipeToDeleteWithConfirmation( modifier = Modifier.fillMaxWidth().animateContentSize(), - onStartToEnd = { accountViewModel.delete(item) }, + onDelete = { accountViewModel.delete(item) }, ) { NoteCompose( item, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 8a710fd46..72f3724dc 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1473,7 +1473,6 @@ Add a Blossom Server Delete all Are you sure you want to delete all drafts? - Are you sure you want to delete this draft? " +%1$s" Stack: